diff --git a/.gitignore b/.gitignore index 2ea4e4db..15ca555d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,31 @@ -config.ini -.idea/ -.vscode/ -__pycache__ +# Royalnet ignores +config*.toml downloads/ -ignored/ -markovmodels/ -logs/ -royalnet.egg-info/ -.pytest_cache/ + + +# Python ignores +**/__pycache__/ dist/ -build/ -venv/ +*.egg-info/ +**/*.pyc + +# PyCharm ignores +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +.idea/**/markdown-navigator.xml +.idea/**/markdown-exported-files.xml +.idea/**/misc.xml +.idea/**/*.iml \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..15a15b21 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..828aba99 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 00000000..db062663 --- /dev/null +++ b/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..502cd4e7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/royalnet.iml b/.idea/royalnet.iml new file mode 100644 index 00000000..3dcd20ac --- /dev/null +++ b/.idea/royalnet.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/royalnet__config_ini_.xml b/.idea/runConfigurations/royalnet__config_ini_.xml new file mode 100644 index 00000000..25897d9a --- /dev/null +++ b/.idea/runConfigurations/royalnet__config_ini_.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 0ad25db4..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 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 Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are 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. - - 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. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - 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 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 work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - 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 AGPL, see -. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b88616a9..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -recursive-include **/templates *.html -recursive-include **/static * -prune venv diff --git a/README.md b/README.md index 845658bd..151cf67b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ # `royalnet` [![PyPI](https://img.shields.io/pypi/v/royalnet.svg)](https://pypi.org/project/royalnet/) -The fifth rewrite of the Royal Network! +A multipurpose bot framework and webserver -It has a lot of submodules, many of which may be used in other bots. +## About -[Documentation available here](https://royal-games.github.io/royalnet/html/index.html). +`royalnet` is a Python framework that allows you to create interconnected modular chat bots accessible through multiple interfaces (such as Telegram or Discord), and also modular websites that can be connected with the bots. + +### Supported bot platforms + +- [Telegram](https://core.telegram.org/bots) +- [Discord](https://discordapp.com/developers/docs/) + +## Installing + +Installing `royalnet` is a bit messy right now; please wait for the release of `5.1`! + +## Documentation + +`royalnet`'s documentation is available [here](https://gh.steffo.eu/royalnet). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index c4e58f58..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,16 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| Latest | :white_check_mark: | -| Older | :x: | - -## Reporting a Vulnerability - -I think you should be able to report vulnerabilities on GitHub. - -If you report a vulnerability there I'll try to fix it as soon as I have time. - -Thanks for your help! diff --git a/connect_to_main_server.bat b/connect_to_main_server.bat deleted file mode 100644 index b969fc6f..00000000 --- a/connect_to_main_server.bat +++ /dev/null @@ -1 +0,0 @@ -ssh -i "D:\Chiavi e robe\Terza.pem" root@scaleway.steffo.eu diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e1b05b08..00000000 --- a/docs/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# `docs` - -Leave the `index.html` and the `.nojekyll` files here, otherwise GitHub pages will break. - -To update the docs, **delete all folders inside this one** and run `make html` in the `docs_source` folder. diff --git a/docs/doctrees/apireference.doctree b/docs/doctrees/apireference.doctree deleted file mode 100644 index 6371b865..00000000 Binary files a/docs/doctrees/apireference.doctree and /dev/null differ diff --git a/docs/doctrees/creatingacommand.doctree b/docs/doctrees/creatingacommand.doctree deleted file mode 100644 index 3b0af7cb..00000000 Binary files a/docs/doctrees/creatingacommand.doctree and /dev/null differ diff --git a/docs/doctrees/environment.pickle b/docs/doctrees/environment.pickle deleted file mode 100644 index 64faa982..00000000 Binary files a/docs/doctrees/environment.pickle and /dev/null differ diff --git a/docs/doctrees/index.doctree b/docs/doctrees/index.doctree deleted file mode 100644 index 54662b5a..00000000 Binary files a/docs/doctrees/index.doctree and /dev/null differ diff --git a/docs/doctrees/runningroyalnet.doctree b/docs/doctrees/runningroyalnet.doctree deleted file mode 100644 index 0205cd88..00000000 Binary files a/docs/doctrees/runningroyalnet.doctree and /dev/null differ diff --git a/docs/html/.buildinfo b/docs/html/.buildinfo deleted file mode 100644 index 1166075a..00000000 --- a/docs/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: b57ffa99137bc3f723f5a94d2c38b963 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/html/_sources/apireference.rst.txt b/docs/html/_sources/apireference.rst.txt deleted file mode 100644 index a8b13455..00000000 --- a/docs/html/_sources/apireference.rst.txt +++ /dev/null @@ -1,11 +0,0 @@ -API Reference -==================================== - -These pages were automatically generated from docstrings in code. - -They might be outdated, or incomplete. - -.. automodule:: royalnet - :members: - :undoc-members: - :private-members: diff --git a/docs/html/_sources/creatingacommand.rst.txt b/docs/html/_sources/creatingacommand.rst.txt deleted file mode 100644 index 7f72cf7b..00000000 --- a/docs/html/_sources/creatingacommand.rst.txt +++ /dev/null @@ -1,381 +0,0 @@ -.. currentmodule:: royalnet.commands - -Royalnet Commands -==================================== - -A Royalnet Command is a small script that is run whenever a specific message is sent to a Royalnet interface. - -A Command code looks like this: :: - - from royalnet.commands import Command - - class PingCommand(Command): - name = "ping" - - description = "Play ping-pong with the bot." - - def __init__(self, interface): - # This code is run just once, while the bot is starting - super().__init__() - - async def run(self, args, data): - # This code is run every time the command is called - await data.reply("Pong!") - -Creating a new Command ------------------------------------- - -First, think of a ``name`` for your command. -It's the name your command will be called with: for example, the "spaghetti" command will be called by typing **/spaghetti** in chat. -Try to keep the name as short as possible, while staying specific enough so no other command will have the same name. - -Next, create a new Python file with the ``name`` you have thought of. -The previously mentioned "spaghetti" command should have a file called ``spaghetti.py``. - -Then, in the first row of the file, import the :py:class:`Command` class from royalnet, and create a new class inheriting from it: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - ... - -Inside the class, override the attributes ``name`` and ``description`` with respectively the **name of the command** and a **small description of what the command will do**: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - -Now override the :py:meth:`Command.run` method, adding the code you want the bot to run when the command is called. - -To send a message in the chat the command was called in, you can use the :py:meth:`CommandData.reply` method: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - async def run(self, args, data): - await data.reply("ðŸ") - -And... it's done! The command is ready to be added to a Pack! - -Command arguments ------------------------------------- - -A command can have some arguments passed by the user: for example, on Telegram an user may type `/spaghetti carbonara al-dente` -to pass the :py:class:`str` `"carbonara al-dente"` to the command code. - -These arguments can be accessed in multiple ways through the ``args`` parameter passed to the :py:meth:`Command.run` -method. - -If you want your command to use arguments, override the ``syntax`` class attribute with a brief description of the -syntax of your command, possibly using {curly braces} for required arguments and [square brackets] for optional -ones. :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "(requestedpasta)" - - async def run(self, args, data): - await data.reply(f"ðŸ Here's your {args[0]}!") - - -Direct access -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can consider arguments as if they were separated by spaces. - -You can then access command arguments directly by number as if the args object was a list of :py:class:`str`. - -If you request an argument with a certain number, but the argument does not exist, an -:py:exc:`royalnet.error.InvalidInputError` is raised, making the arguments accessed in this way **required**. :: - - args[0] - # "carbonara" - - args[1] - # "al-dente" - - args[2] - # InvalidInputError() is raised - -Optional access -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't want arguments to be required, you can access them through the :py:meth:`CommandArgs.optional` method: it -will return :py:const:`None` if the argument wasn't passed, making it **optional**. :: - - args.optional(0) - # "carbonara" - - args.optional(1) - # "al-dente" - - args.optional(2) - # None - -You can specify a default result too, so that the method will return it instead of returning :py:const:`None`: :: - - args.optional(2, default="banana") - # "banana" - -Full string -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want the full argument string, you can use the :py:meth:`CommandArgs.joined` method. :: - - args.joined() - # "carbonara al-dente" - -You can specify a minimum number of arguments too, so that an :py:exc:`InvalidInputError` will be -raised if not enough arguments are present: :: - - args.joined(require_at_least=3) - # InvalidInputError() is raised - -Regular expressions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For more complex commands, you may want to get arguments through `regular expressions `_. - -You can then use the :py:meth:`CommandArgs.match` method, which tries to match a pattern to the command argument string, -which returns a tuple of the matched groups and raises an :py:exc:`InvalidInputError` if there is no match. - -To match a pattern, :py:func:`re.match` is used, meaning that Python will try to match only at the beginning of the string. :: - - args.match(r"(carb\w+)") - # ("carbonara",) - - args.match(r"(al-\w+)") - # InvalidInputError() is raised - - args.match(r"\s*(al-\w+)") - # ("al-dente",) - - args.match(r"\s*(carb\w+)\s*(al-\w+)") - # ("carbonara", "al-dente") - -Raising errors ---------------------------------------------- - -If you want to display an error message to the user, you can raise a :py:exc:`CommandError` using the error message as argument: :: - - if not kitchen.is_open(): - raise CommandError("The kitchen is closed. Come back later!") - -You can also manually raise :py:exc:`InvalidInputError` to redisplay the command syntax, along with your error message: :: - - if args[0] not in allowed_pasta: - raise InvalidInputError("The specified pasta type is invalid.") - -If you need a Royalnet feature that's not available on the current interface, you can raise an -:py:exc:`UnsupportedError` with a brief description of what's missing: :: - - if interface.name != "telegram": - raise UnsupportedError("This command can only be run on Telegram interfaces.") - -Running code at the initialization of the bot ---------------------------------------------- - -You can run code while the bot is starting by overriding the :py:meth:`Command.__init__` function. - -You should keep the ``super().__init__(interface)`` call at the start of it, so that the :py:class:`Command` instance is -initialized properly, then add your code after it. - -You can add fields to the command to keep **shared data between multiple command calls** (but not bot restarts): it may -be useful for fetching external static data and keeping it until the bot is restarted, or to store references to all the -:py:class:`asyncio.Task` started by the bot. :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "{pasta}" - - def __init__(self, interface): - super().__init__(interface) - self.requested_pasta = [] - - async def run(self, args, data): - pasta = args[0] - if pasta in self.requested_pasta: - await data.reply(f"âš ï¸ This pasta was already requested before.") - return - self.requested_pasta.append(pasta) - await data.reply(f"ðŸ Here's your {pasta}!") - - -Coroutines and slow operations ------------------------------------- - -You may have noticed that in the previous examples we used ``await data.reply("ðŸ")`` instead of just ``data.reply("ðŸ")``. - -This is because :py:meth:`CommandData.reply` isn't a simple method: it is a coroutine, a special kind of function that -can be executed separately from the rest of the code, allowing the bot to do other things in the meantime. - -By adding the ``await`` keyword before the ``data.reply("ðŸ")``, we tell the bot that it can do other things, like -receiving new messages, while the message is being sent. - -You should avoid running slow normal functions inside bot commands, as they will stop the bot from working until they -are finished and may cause bugs in other parts of the code! :: - - async def run(self, args, data): - # Don't do this! - image = download_1_terabyte_of_spaghetti("right_now", from="italy") - ... - -If the slow function you want does not cause any side effect, you can wrap it with the :py:func:`royalnet.utils.asyncify` -function: :: - - async def run(self, args, data): - # If the called function has no side effect, you can do this! - image = await asyncify(download_1_terabyte_of_spaghetti, "right_now", from="italy") - ... - -Avoid using :py:func:`time.sleep` function, as it is considered a slow operation: use instead :py:func:`asyncio.sleep`, -a coroutine that does the same exact thing. - -Delete the invoking message ------------------------------------- - -The invoking message of a command is the message that the user sent that the bot recognized as a command; for example, -the message ``/spaghetti carbonara`` is the invoking message for the ``spaghetti`` command run. - -You can have the bot delete the invoking message for a command by calling the :py:class:`CommandData.delete_invoking` -method: :: - - async def run(self, args, data): - await data.delete_invoking() - -Not all interfaces support deleting messages; by default, if the interface does not support deletions, the call is -ignored. - -You can have the method raise an error if the message can't be deleted by setting the ``error_if_unavailable`` parameter -to True: :: - - async def run(self, args, data): - try: - await data.delete_invoking(error_if_unavailable=True) - except royalnet.error.UnsupportedError: - await data.reply("🚫 The message could not be deleted.") - else: - await data.reply("✅ The message was deleted!") - -Using the database ------------------------------------- - -Bots can be connected to a PostgreSQL database through a special SQLAlchemy interface called -:py:class:`royalnet.database.Alchemy`. - -If the connection is established, the ``self.alchemy`` and ``data.session`` fields will be -available for use in commands. - -``self.interface.alchemy`` is an instance of :py:class:`royalnet.database.Alchemy`, which contains the -:py:class:`sqlalchemy.engine.Engine`, metadata and tables, while ``data.session`` is a -:py:class:`sqlalchemy.orm.session.Session`, and can be interacted in the same way as one. - -If you want to use :py:class:`royalnet.database.Alchemy` in your command, you should override the -``tables`` field with the :py:class:`set` of Alchemy tables you need. :: - - from royalnet.commands import Command - from royalnet.packs.common.tables import User - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "{pasta}" - - tables = {User} - - ... - -Querying the database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can :py:class:`sqlalchemy.orm.query.Query` the database using the SQLAlchemy ORM. - -The SQLAlchemy tables can be found inside :py:class:`royalnet.database.Alchemy` with the same name they were created -from, if they were specified in ``tables``. :: - - query = data.session.query(User) - -Adding filters to the query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can filter the query results with the :py:meth:`sqlalchemy.orm.query.Query.filter` method. - -.. note:: Remember to always use a table column as first comparision element, as it won't work otherwise. - -:: - - query = query.filter(User.role == "Member") - - -Ordering the results of a query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can order the query results in **ascending order** with the :py:meth:`sqlalchemy.orm.query.Query.order_by` method. :: - - query = query.order_by(User.username) - -Additionally, you can append the `.desc()` method to a table column to sort in **descending order**: :: - - query = query.order_by(User.username.desc()) - -Fetching the results of a query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can fetch the query results with the :py:meth:`sqlalchemy.orm.query.Query.all`, -:py:meth:`sqlalchemy.orm.query.Query.first`, :py:meth:`sqlalchemy.orm.query.Query.one` and -:py:meth:`sqlalchemy.orm.query.Query.one_or_none` methods. - -Remember to use :py:func:`royalnet.utils.asyncify` when fetching results, as it may take a while! - -Use :py:meth:`sqlalchemy.orm.query.Query.all` if you want a :py:class:`list` of **all results**: :: - - results: list = await asyncify(query.all) - -Use :py:meth:`sqlalchemy.orm.query.Query.first` if you want **the first result** of the list, or :py:const:`None` if -there are no results: :: - - result: typing.Union[..., None] = await asyncify(query.first) - -Use :py:meth:`sqlalchemy.orm.query.Query.one` if you expect to have **a single result**, and you want the command to -raise an error if any different number of results is returned: :: - - result: ... = await asyncify(query.one) # Raises an error if there are no results or more than a result. - -Use :py:meth:`sqlalchemy.orm.query.Query.one_or_none` if you expect to have **a single result**, or **nothing**, and -if you want the command to raise an error if the number of results is greater than one. :: - - result: typing.Union[..., None] = await asyncify(query.one_or_none) # Raises an error if there is more than a result. - -More Alchemy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can read more about :py:mod:`sqlalchemy` at their `website `_. - -Comunicating via Royalnet ------------------------------------- - -This section is not documented yet. - -Adding the command to a Pack ------------------------------------- - -This section is not documented yet. \ No newline at end of file diff --git a/docs/html/_sources/index.rst.txt b/docs/html/_sources/index.rst.txt deleted file mode 100644 index 3e909434..00000000 --- a/docs/html/_sources/index.rst.txt +++ /dev/null @@ -1,18 +0,0 @@ -royalnet -==================================== - -Welcome to the documentation of Royalnet! - -.. toctree:: - :maxdepth: 3 - - runningroyalnet - creatingacommand - apireference - - -Some useful links ------------------------------------- - -* `Royalnet on GitHub `_ -* :ref:`genindex` \ No newline at end of file diff --git a/docs/html/_sources/runningroyalnet.rst.txt b/docs/html/_sources/runningroyalnet.rst.txt deleted file mode 100644 index 7262563b..00000000 --- a/docs/html/_sources/runningroyalnet.rst.txt +++ /dev/null @@ -1,72 +0,0 @@ -.. currentmodule:: royalnet - -Running Royalnet -==================================== - -To run a ``royalnet`` instance, you have first to download the package from ``pip``: - -The Keyring ------------------------------------- -:: - - pip install royalnet - - -To run ``royalnet``, you'll have to setup the system keyring. - -On Windows and desktop Linux, this is already configured; -on a headless Linux instance, you'll need to `manually start and unlock the keyring daemon -`_. - -Now you have to create a new ``royalnet`` configuration. Start the configuration wizard: :: - - python -m royalnet.configurator - -You'll be prompted to enter a "secrets name": this is the name of the group of API keys that will be associated with -your bot. Enter a name that you'll be able to remember. :: - - Desired secrets name [__default__]: royalgames - -You'll then be asked for a network password. - -This password is used to connect to the rest of the :py:mod:`royalnet.network`, or, if you're hosting a local Network, -it will be the necessary password to connect to it: :: - - Network password []: cosafaunapesuunafoglia - -Then you'll be asked for a Telegram Bot API token. -You can get one from `@BotFather `_. :: - - Telegram Bot API token []: 000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - -The next prompt will ask for a Discord Bot API token. -You can get one at the `Discord Developers Portal `_. :: - - Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA - -Now the configurator will ask you for a Imgur API token. -`Register an application `_ on Imgur to be supplied one. -The token should be of type "anonymous usage without user authorization". :: - - Imgur API token []: aaaaaaaaaaaaaaa - -Next, you'll be asked for a Sentry DSN. You probably won't have one, so just ignore it and press enter. :: - - Sentry DSN []: - -Now that all tokens are configured, you're ready to launch the bot! - -Running the bots ------------------------------------- - -You can run the main ``royalnet`` process by running: :: - - python3.7 -m royalnet - -To see all available options, you can run: :: - - python3.7 -m royalnet --help - -.. note:: All royalnet options should be specified **after** the word ``royalnet``, or else they will be passed to - the Python interpreter. - diff --git a/docs/html/_static/basic.css b/docs/html/_static/basic.css deleted file mode 100644 index ea6972d5..00000000 --- a/docs/html/_static/basic.css +++ /dev/null @@ -1,764 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 450px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a.brackets:before, -span.brackets > a:before{ - content: "["; -} - -a.brackets:after, -span.brackets > a:after { - content: "]"; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > p:first-child, -td > p:first-child { - margin-top: 0px; -} - -th > p:last-child, -td > p:last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist td { - vertical-align: top; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -li > p:first-child { - margin-top: 0px; -} - -li > p:last-child { - margin-bottom: 0px; -} - -dl.footnote > dt, -dl.citation > dt { - float: left; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dt:after { - content: ":"; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > p:first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0.5em; - content: ":"; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -div.code-block-caption { - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -div.code-block-caption + div > div.highlight > pre { - margin-top: 0; -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - padding: 1em 1em 0; -} - -div.literal-block-wrapper div.highlight { - margin: 0; -} - -code.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -code.descclassname { - background-color: transparent; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: relative; - left: 0px; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/html/_static/css/badge_only.css b/docs/html/_static/css/badge_only.css deleted file mode 100644 index 3c33cef5..00000000 --- a/docs/html/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} diff --git a/docs/html/_static/css/theme.css b/docs/html/_static/css/theme.css deleted file mode 100644 index aed8cef0..00000000 --- a/docs/html/_static/css/theme.css +++ /dev/null @@ -1,6 +0,0 @@ -/* sphinx_rtd_theme version 0.4.3 | MIT license */ -/* Built 20190212 16:02 */ -*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content .code-block-caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content .code-block-caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:"ï€"}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:"ï€"}.fa-search-plus:before{content:""}.fa-search-minus:before{content:"ï€"}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:"ï€"}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:"ï€"}.fa-map-marker:before{content:"ï"}.fa-adjust:before{content:"ï‚"}.fa-tint:before{content:"ïƒ"}.fa-edit:before,.fa-pencil-square-o:before{content:"ï„"}.fa-share-square-o:before{content:"ï…"}.fa-check-square-o:before{content:"ï†"}.fa-arrows:before{content:"ï‡"}.fa-step-backward:before{content:"ïˆ"}.fa-fast-backward:before{content:"ï‰"}.fa-backward:before{content:"ïŠ"}.fa-play:before{content:"ï‹"}.fa-pause:before{content:"ïŒ"}.fa-stop:before{content:"ï"}.fa-forward:before{content:"ïŽ"}.fa-fast-forward:before{content:"ï"}.fa-step-forward:before{content:"ï‘"}.fa-eject:before{content:"ï’"}.fa-chevron-left:before{content:"ï“"}.fa-chevron-right:before{content:"ï”"}.fa-plus-circle:before{content:"ï•"}.fa-minus-circle:before{content:"ï–"}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:"ï—"}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:"ï˜"}.fa-question-circle:before{content:"ï™"}.fa-info-circle:before{content:"ïš"}.fa-crosshairs:before{content:"ï›"}.fa-times-circle-o:before{content:"ïœ"}.fa-check-circle-o:before{content:"ï"}.fa-ban:before{content:"ïž"}.fa-arrow-left:before{content:"ï "}.fa-arrow-right:before{content:"ï¡"}.fa-arrow-up:before{content:"ï¢"}.fa-arrow-down:before{content:"ï£"}.fa-mail-forward:before,.fa-share:before{content:"ï¤"}.fa-expand:before{content:"ï¥"}.fa-compress:before{content:"ï¦"}.fa-plus:before{content:"ï§"}.fa-minus:before{content:"ï¨"}.fa-asterisk:before{content:"ï©"}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:"ïª"}.fa-gift:before{content:"ï«"}.fa-leaf:before{content:"ï¬"}.fa-fire:before,.icon-fire:before{content:"ï­"}.fa-eye:before{content:"ï®"}.fa-eye-slash:before{content:"ï°"}.fa-warning:before,.fa-exclamation-triangle:before{content:"ï±"}.fa-plane:before{content:"ï²"}.fa-calendar:before{content:"ï³"}.fa-random:before{content:"ï´"}.fa-comment:before{content:"ïµ"}.fa-magnet:before{content:"ï¶"}.fa-chevron-up:before{content:"ï·"}.fa-chevron-down:before{content:"ï¸"}.fa-retweet:before{content:"ï¹"}.fa-shopping-cart:before{content:"ïº"}.fa-folder:before{content:"ï»"}.fa-folder-open:before{content:"ï¼"}.fa-arrows-v:before{content:"ï½"}.fa-arrows-h:before{content:"ï¾"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"ï‚€"}.fa-twitter-square:before{content:"ï‚"}.fa-facebook-square:before{content:"ï‚‚"}.fa-camera-retro:before{content:""}.fa-key:before{content:"ï‚„"}.fa-gears:before,.fa-cogs:before{content:"ï‚…"}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:"ï‚Š"}.fa-sign-out:before{content:"ï‚‹"}.fa-linkedin-square:before{content:"ï‚Œ"}.fa-thumb-tack:before{content:"ï‚"}.fa-external-link:before{content:"ï‚Ž"}.fa-sign-in:before{content:"ï‚"}.fa-trophy:before{content:"ï‚‘"}.fa-github-square:before{content:"ï‚’"}.fa-upload:before{content:"ï‚“"}.fa-lemon-o:before{content:"ï‚”"}.fa-phone:before{content:"ï‚•"}.fa-square-o:before{content:"ï‚–"}.fa-bookmark-o:before{content:"ï‚—"}.fa-phone-square:before{content:""}.fa-twitter:before{content:"ï‚™"}.fa-facebook-f:before,.fa-facebook:before{content:"ï‚š"}.fa-github:before,.icon-github:before{content:"ï‚›"}.fa-unlock:before{content:"ï‚œ"}.fa-credit-card:before{content:"ï‚"}.fa-feed:before,.fa-rss:before{content:"ï‚ž"}.fa-hdd-o:before{content:"ï‚ "}.fa-bullhorn:before{content:"ï‚¡"}.fa-bell:before{content:""}.fa-certificate:before{content:"ï‚£"}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:"ï‚¥"}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:"ï‚©"}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:"ï‚«"}.fa-globe:before{content:""}.fa-wrench:before{content:"ï‚­"}.fa-tasks:before{content:"ï‚®"}.fa-filter:before{content:"ï‚°"}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:"ïƒ"}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:"ïƒ"}.fa-table:before{content:""}.fa-magic:before{content:"ïƒ"}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:"ïƒ"}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:"ï‚¢"}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:"ï„€"}.fa-angle-double-right:before{content:"ï„"}.fa-angle-double-up:before{content:"ï„‚"}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:"ï„„"}.fa-angle-right:before{content:"ï„…"}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:"ï„Š"}.fa-mobile-phone:before,.fa-mobile:before{content:"ï„‹"}.fa-circle-o:before{content:"ï„Œ"}.fa-quote-left:before{content:"ï„"}.fa-quote-right:before{content:"ï„Ž"}.fa-spinner:before{content:"ï„"}.fa-circle:before{content:"ï„‘"}.fa-mail-reply:before,.fa-reply:before{content:"ï„’"}.fa-github-alt:before{content:"ï„“"}.fa-folder-o:before{content:"ï„”"}.fa-folder-open-o:before{content:"ï„•"}.fa-smile-o:before{content:""}.fa-frown-o:before{content:"ï„™"}.fa-meh-o:before{content:"ï„š"}.fa-gamepad:before{content:"ï„›"}.fa-keyboard-o:before{content:"ï„œ"}.fa-flag-o:before{content:"ï„"}.fa-flag-checkered:before{content:"ï„ž"}.fa-terminal:before{content:"ï„ "}.fa-code:before{content:"ï„¡"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"ï„¢"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"ï„£"}.fa-location-arrow:before{content:""}.fa-crop:before{content:"ï„¥"}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:"ï„©"}.fa-exclamation:before{content:""}.fa-superscript:before{content:"ï„«"}.fa-subscript:before{content:""}.fa-eraser:before{content:"ï„­"}.fa-puzzle-piece:before{content:"ï„®"}.fa-microphone:before{content:"ï„°"}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:"ï„´"}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:"ï„·"}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:"ï„»"}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:"ï…€"}.fa-ellipsis-h:before{content:"ï…"}.fa-ellipsis-v:before{content:"ï…‚"}.fa-rss-square:before{content:"ï…ƒ"}.fa-play-circle:before{content:"ï…„"}.fa-ticket:before{content:"ï……"}.fa-minus-square:before{content:"ï…†"}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:"ï…‡"}.fa-level-up:before{content:"ï…ˆ"}.fa-level-down:before{content:"ï…‰"}.fa-check-square:before{content:"ï…Š"}.fa-pencil-square:before{content:"ï…‹"}.fa-external-link-square:before{content:"ï…Œ"}.fa-share-square:before{content:"ï…"}.fa-compass:before{content:"ï…Ž"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"ï…"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"ï…‘"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"ï…’"}.fa-euro:before,.fa-eur:before{content:"ï…“"}.fa-gbp:before{content:"ï…”"}.fa-dollar:before,.fa-usd:before{content:"ï…•"}.fa-rupee:before,.fa-inr:before{content:"ï…–"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"ï…—"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"ï…˜"}.fa-won:before,.fa-krw:before{content:"ï…™"}.fa-bitcoin:before,.fa-btc:before{content:"ï…š"}.fa-file:before{content:"ï…›"}.fa-file-text:before{content:"ï…œ"}.fa-sort-alpha-asc:before{content:"ï…"}.fa-sort-alpha-desc:before{content:"ï…ž"}.fa-sort-amount-asc:before{content:"ï… "}.fa-sort-amount-desc:before{content:"ï…¡"}.fa-sort-numeric-asc:before{content:"ï…¢"}.fa-sort-numeric-desc:before{content:"ï…£"}.fa-thumbs-up:before{content:"ï…¤"}.fa-thumbs-down:before{content:"ï…¥"}.fa-youtube-square:before{content:"ï…¦"}.fa-youtube:before{content:"ï…§"}.fa-xing:before{content:"ï…¨"}.fa-xing-square:before{content:"ï…©"}.fa-youtube-play:before{content:"ï…ª"}.fa-dropbox:before{content:"ï…«"}.fa-stack-overflow:before{content:"ï…¬"}.fa-instagram:before{content:"ï…­"}.fa-flickr:before{content:"ï…®"}.fa-adn:before{content:"ï…°"}.fa-bitbucket:before,.icon-bitbucket:before{content:"ï…±"}.fa-bitbucket-square:before{content:"ï…²"}.fa-tumblr:before{content:"ï…³"}.fa-tumblr-square:before{content:"ï…´"}.fa-long-arrow-down:before{content:"ï…µ"}.fa-long-arrow-up:before{content:"ï…¶"}.fa-long-arrow-left:before{content:"ï…·"}.fa-long-arrow-right:before{content:"ï…¸"}.fa-apple:before{content:"ï…¹"}.fa-windows:before{content:"ï…º"}.fa-android:before{content:"ï…»"}.fa-linux:before{content:"ï…¼"}.fa-dribbble:before{content:"ï…½"}.fa-skype:before{content:"ï…¾"}.fa-foursquare:before{content:""}.fa-trello:before{content:"ï†"}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:"ï†"}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:"ï†"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:"ï†"}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:"ï‡"}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"ï‡"}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"ï‡"}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:"ï‡"}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:"ïˆ"}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:"ïˆ"}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:"ïˆ"}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:"ïˆ"}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"ï‰"}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:"ï‰"}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:"ï‰"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:"ï‰"}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:"ïŠ"}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:"ïŠ"}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:"ïŠ"}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:"ïŠ"}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:"ï‹€"}.fa-id-badge:before{content:"ï‹"}.fa-drivers-license:before,.fa-id-card:before{content:"ï‹‚"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:"ï‹„"}.fa-free-code-camp:before{content:"ï‹…"}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"ï‹Š"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"ï‹‹"}.fa-shower:before{content:"ï‹Œ"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"ï‹"}.fa-podcast:before{content:"ï‹Ž"}.fa-window-maximize:before{content:"ï‹"}.fa-window-minimize:before{content:"ï‹‘"}.fa-window-restore:before{content:"ï‹’"}.fa-times-rectangle:before,.fa-window-close:before{content:"ï‹“"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"ï‹”"}.fa-bandcamp:before{content:"ï‹•"}.fa-grav:before{content:"ï‹–"}.fa-etsy:before{content:"ï‹—"}.fa-imdb:before{content:""}.fa-ravelry:before{content:"ï‹™"}.fa-eercast:before{content:"ï‹š"}.fa-microchip:before{content:"ï‹›"}.fa-snowflake-o:before{content:"ï‹œ"}.fa-superpowers:before{content:"ï‹"}.fa-wpexplorer:before{content:"ï‹ž"}.fa-meetup:before{content:"ï‹ "}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.3576515979%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.3576515979%;width:48.821174201%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.3576515979%;width:31.7615656014%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{position:absolute;content:"";display:block;left:0;top:0;width:36px;height:12px;border-radius:4px;background:#ccc;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27AE60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:.3em;display:block}.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:before,.wy-breadcrumbs:after{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#3a7ca8;height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin:12px 0 0 0;display:block;font-weight:bold;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a{color:#404040}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980B9;text-align:center;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:gray}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:gray}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{width:100%}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1100px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"ï‚Ž";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;display:block;overflow:auto}.rst-content pre.literal-block,.rst-content div[class^='highlight']{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px 0}.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{padding:0px;border:none;margin:0}.rst-content div[class^='highlight'] td.code{width:100%}.rst-content .linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;display:block;overflow:auto}.rst-content div[class^='highlight'] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:12px;line-height:1.4}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^='highlight'],.rst-content div[class^='highlight'] pre{white-space:pre-wrap}}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .section ol p:last-child,.rst-content .section ul p:last-child{margin-bottom:24px}.rst-content .line-block{margin-left:0px;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink{visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after,.rst-content .code-block-caption .headerlink:after{content:"ïƒ";font-family:FontAwesome}.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after,.rst-content .code-block-caption:hover .headerlink:after{visibility:visible}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:baseline;position:relative;top:-0.4em;line-height:0;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:gray}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}.rst-content table.docutils td .last,.rst-content table.docutils td .last :last-child{margin-bottom:0}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content tt,.rst-content tt,.rst-content code{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content pre,.rst-content kbd,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold;margin-bottom:12px}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-regular.eot");src:url("../fonts/Lato/lato-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-regular.woff2") format("woff2"),url("../fonts/Lato/lato-regular.woff") format("woff"),url("../fonts/Lato/lato-regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bold.eot");src:url("../fonts/Lato/lato-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bold.woff2") format("woff2"),url("../fonts/Lato/lato-bold.woff") format("woff"),url("../fonts/Lato/lato-bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bolditalic.eot");src:url("../fonts/Lato/lato-bolditalic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bolditalic.woff2") format("woff2"),url("../fonts/Lato/lato-bolditalic.woff") format("woff"),url("../fonts/Lato/lato-bolditalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-italic.eot");src:url("../fonts/Lato/lato-italic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-italic.woff2") format("woff2"),url("../fonts/Lato/lato-italic.woff") format("woff"),url("../fonts/Lato/lato-italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:url("../fonts/RobotoSlab/roboto-slab.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.ttf") format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.ttf") format("truetype")} diff --git a/docs/html/_static/doctools.js b/docs/html/_static/doctools.js deleted file mode 100644 index b33f87fc..00000000 --- a/docs/html/_static/doctools.js +++ /dev/null @@ -1,314 +0,0 @@ -/* - * doctools.js - * ~~~~~~~~~~~ - * - * Sphinx JavaScript utilities for all documentation. - * - * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - -/** - * make the code below compatible with browsers without - * an installed firebug like debugger -if (!window.console || !console.firebug) { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", - "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", - "profile", "profileEnd"]; - window.console = {}; - for (var i = 0; i < names.length; ++i) - window.console[names[i]] = function() {}; -} - */ - -/** - * small helper function to urldecode strings - */ -jQuery.urldecode = function(x) { - return decodeURIComponent(x).replace(/\+/g, ' '); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} - -/** - * Small JavaScript module for the documentation. - */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { - this.initOnKeyListeners(); - } - }, - - /** - * i18n support - */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, - LOCALE : 'unknown', - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated === 'undefined') - return string; - return (typeof translated === 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated === 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; - }, - - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); - }, - - /** - * workaround a firefox stupidity - * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); - }, - - /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - if (!body.length) { - body = $('body'); - } - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, - - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) === 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); - }, - - /** - * make the url absolute - */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; - }, - - /** - * get the current relative url - */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this === '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); - }, - - initOnKeyListeners: function() { - $(document).keyup(function(event) { - var activeElementType = document.activeElement.tagName; - // don't navigate when in search box or textarea - if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { - switch (event.keyCode) { - case 37: // left - var prevHref = $('link[rel="prev"]').prop('href'); - if (prevHref) { - window.location.href = prevHref; - return false; - } - case 39: // right - var nextHref = $('link[rel="next"]').prop('href'); - if (nextHref) { - window.location.href = nextHref; - return false; - } - } - } - }); - } -}; - -// quick alias for translations -_ = Documentation.gettext; - -$(document).ready(function() { - Documentation.init(); -}); diff --git a/docs/html/_static/documentation_options.js b/docs/html/_static/documentation_options.js deleted file mode 100644 index 6d865102..00000000 --- a/docs/html/_static/documentation_options.js +++ /dev/null @@ -1,10 +0,0 @@ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '', - LANGUAGE: 'None', - COLLAPSE_INDEX: false, - FILE_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false -}; \ No newline at end of file diff --git a/docs/html/_static/file.png b/docs/html/_static/file.png deleted file mode 100644 index a858a410..00000000 Binary files a/docs/html/_static/file.png and /dev/null differ diff --git a/docs/html/_static/fonts/Inconsolata-Bold.ttf b/docs/html/_static/fonts/Inconsolata-Bold.ttf deleted file mode 100644 index 809c1f58..00000000 Binary files a/docs/html/_static/fonts/Inconsolata-Bold.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Inconsolata-Regular.ttf b/docs/html/_static/fonts/Inconsolata-Regular.ttf deleted file mode 100644 index fc981ce7..00000000 Binary files a/docs/html/_static/fonts/Inconsolata-Regular.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Inconsolata.ttf b/docs/html/_static/fonts/Inconsolata.ttf deleted file mode 100644 index 4b8a36d2..00000000 Binary files a/docs/html/_static/fonts/Inconsolata.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato-Bold.ttf b/docs/html/_static/fonts/Lato-Bold.ttf deleted file mode 100644 index 1d23c706..00000000 Binary files a/docs/html/_static/fonts/Lato-Bold.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato-Regular.ttf b/docs/html/_static/fonts/Lato-Regular.ttf deleted file mode 100644 index 0f3d0f83..00000000 Binary files a/docs/html/_static/fonts/Lato-Regular.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.eot b/docs/html/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183a..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.ttf b/docs/html/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d5..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.woff b/docs/html/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51f..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.woff2 b/docs/html/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb195043..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.eot b/docs/html/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d415493..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.ttf b/docs/html/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040b..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.woff b/docs/html/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b9..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d804..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.eot b/docs/html/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f826421..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.ttf b/docs/html/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b2..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.woff b/docs/html/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc0..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.woff2 b/docs/html/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37e..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.eot b/docs/html/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a5..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.ttf b/docs/html/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9e..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.woff b/docs/html/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307ff..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.woff2 b/docs/html/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf98433..00000000 Binary files a/docs/html/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab-Bold.ttf b/docs/html/_static/fonts/RobotoSlab-Bold.ttf deleted file mode 100644 index df5d1df2..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab-Bold.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab-Regular.ttf b/docs/html/_static/fonts/RobotoSlab-Regular.ttf deleted file mode 100644 index eb52a790..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab-Regular.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8efe..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df2..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb60000..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e231..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78a..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a790..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63f..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5b..00000000 Binary files a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.eot b/docs/html/_static/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca9..00000000 Binary files a/docs/html/_static/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.svg b/docs/html/_static/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845e..00000000 --- a/docs/html/_static/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/html/_static/fonts/fontawesome-webfont.ttf b/docs/html/_static/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2f..00000000 Binary files a/docs/html/_static/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.woff b/docs/html/_static/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4..00000000 Binary files a/docs/html/_static/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.woff2 b/docs/html/_static/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc60..00000000 Binary files a/docs/html/_static/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/html/_static/jquery-3.4.1.js b/docs/html/_static/jquery-3.4.1.js deleted file mode 100644 index 773ad95c..00000000 --- a/docs/html/_static/jquery-3.4.1.js +++ /dev/null @@ -1,10598 +0,0 @@ -/*! - * jQuery JavaScript Library v3.4.1 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2019-05-01T21:04Z - */ -( function( global, factory ) { - - "use strict"; - - if ( typeof module === "object" && typeof module.exports === "object" ) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. -"use strict"; - -var arr = []; - -var document = window.document; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -var concat = arr.concat; - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -var support = {}; - -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - return typeof obj === "function" && typeof obj.nodeType !== "number"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module - - - -var - version = "3.4.1", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }, - - // Support: Android <=4.0 only - // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a global context - globalEval: function( code, options ) { - DOMEval( code, { nonce: options && options.nonce } ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - // Support: Android <=4.0 only - trim: function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), -function( i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -} ); - -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v2.3.4 - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://js.foundation/ - * - * Date: 2019-04-08 - */ -(function( window ) { - -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + 1 * new Date(), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf as it's faster than native - // https://jsperf.com/thor-indexof-vs-for/5 - indexOf = function( list, elem ) { - var i = 0, - len = list.length; - for ( ; i < len; i++ ) { - if ( list[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - - // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + - "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + identifier + ")" ), - "CLASS": new RegExp( "^\\.(" + identifier + ")" ), - "TAG": new RegExp( "^(" + identifier + "|[*])" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rhtml = /HTML$/i, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox<24 - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - high < 0 ? - // BMP codepoint - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, - fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }, - - // Used for iframes - // See setDocument() - // Removing the function wrapper causes a "Permission Denied" - // error in IE - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; - }, - { dir: "parentNode", next: "legend" } - ); - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( !seed ) { - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - context = context || document; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { - - // ID selector - if ( (m = match[1]) ) { - - // Document context - if ( nodeType === 9 ) { - if ( (elem = context.getElementById( m )) ) { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - - // Element context - } else { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( newContext && (elem = newContext.getElementById( m )) && - contains( context, elem ) && - elem.id === m ) { - - results.push( elem ); - return results; - } - } - - // Type selector - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( (m = match[3]) && support.getElementsByClassName && - context.getElementsByClassName ) { - - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( support.qsa && - !nonnativeSelectorCache[ selector + " " ] && - (!rbuggyQSA || !rbuggyQSA.test( selector )) && - - // Support: IE 8 only - // Exclude object elements - (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && rdescend.test( selector ) ) { - - // Capture the context ID, setting it first if necessary - if ( (nid = context.getAttribute( "id" )) ) { - nid = nid.replace( rcssescape, fcssescape ); - } else { - context.setAttribute( "id", (nid = expando) ); - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[i] = "#" + nid + " " + toSelector( groups[i] ); - } - newSelector = groups.join( "," ); - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key + " " ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement("fieldset"); - - try { - return !!fn( el ); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - // release memory in IE - el = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = arr.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - a.sourceIndex - b.sourceIndex; - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11 - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - /* jshint -W018 */ - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - var namespace = elem.namespaceURI, - docElem = (elem.ownerDocument || elem).documentElement; - - // Support: IE <=8 - // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes - // https://bugs.jquery.com/ticket/4833 - return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; - - // Return early if doc is invalid or already selected - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Update global variables - document = doc; - docElem = document.documentElement; - documentIsHTML = !isXML( document ); - - // Support: IE 9-11, Edge - // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) - if ( preferredDoc !== document && - (subWindow = document.defaultView) && subWindow.top !== subWindow ) { - - // Support: IE 11, Edge - if ( subWindow.addEventListener ) { - subWindow.addEventListener( "unload", unloadHandler, false ); - - // Support: IE 9 - 10 only - } else if ( subWindow.attachEvent ) { - subWindow.attachEvent( "onunload", unloadHandler ); - } - } - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties - // (excepting IE8 booleans) - support.attributes = assert(function( el ) { - el.className = "i"; - return !el.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( el ) { - el.appendChild( document.createComment("") ); - return !el.getElementsByTagName("*").length; - }); - - // Support: IE<9 - support.getElementsByClassName = rnative.test( document.getElementsByClassName ); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function( el ) { - docElem.appendChild( el ).id = expando; - return !document.getElementsByName || !document.getElementsByName( expando ).length; - }); - - // ID filter and find - if ( support.getById ) { - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode("id"); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( (elem = elems[i++]) ) { - node = elem.getAttributeNode("id"); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else if ( support.qsa ) { - return context.querySelectorAll( tag ); - } - } : - - function( tag, context ) { - var elem, - tmp = [], - i = 0, - // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See https://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( el ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // https://bugs.jquery.com/ticket/12359 - docElem.appendChild( el ).innerHTML = "" + - ""; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( el.querySelectorAll("[msallowcapture^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !el.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push("~="); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !el.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - - // Support: Safari 8+, iOS 8+ - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push(".#.+[+~]"); - } - }); - - assert(function( el ) { - el.innerHTML = "" + - ""; - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = document.createElement("input"); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( el.querySelectorAll("[name=d]").length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( el.querySelectorAll(":enabled").length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE9-11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - docElem.appendChild( el ).disabled = true; - if ( el.querySelectorAll(":disabled").length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - el.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( el ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( el, "*" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( el, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully self-exclusive - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { - return -1; - } - if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - return a === document ? -1 : - b === document ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return document; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - if ( support.matchesSelector && documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch (e) { - nonnativeSelectorCache( expr, true ); - } - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null; -}; - -Sizzle.escape = function( sel ) { - return (sel + "").replace( rcssescape, fcssescape ); -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - while ( (node = elem[i++]) ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[6] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[3] ) { - match[2] = match[4] || match[5] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, uniqueCache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) { - - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - - // ...in a gzip-friendly way - node = parent; - outerCache = node[ expando ] || (node[ expando ] = {}); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - // Use previously-cached element index if available - if ( useCache ) { - // ...in a gzip-friendly way - node = elem; - outerCache = node[ expando ] || (node[ expando ] = {}); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ expando ] || (node[ expando ] = {}); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); - - uniqueCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - // Don't keep the element (issue #299) - input[0] = null; - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": createDisabledPseudo( false ), - "disabled": createDisabledPseudo( true ), - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? - argument + length : - argument > length ? - length : - argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( (tokens = []) ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, uniqueCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); - - if ( skip && skip === elem.nodeName.toLowerCase() ) { - elem = elem[ dir ] || elem; - } else if ( (oldCache = uniqueCache[ key ]) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return (newCache[ 2 ] = oldCache[ 2 ]); - } else { - // Reuse newcache so results back-propagate to previous elements - uniqueCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - // Avoid hanging onto element (issue #299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), - len = elems.length; - - if ( outermost ) { - outermostContext = context === document || context || outermost; - } - - // Add elements passing elementMatchers directly to results - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - if ( !context && elem.ownerDocument !== document ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context || document, xml) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -// Support: Chrome 14-35+ -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( el ) { - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement("fieldset") ) & 1; -}); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( el ) { - el.innerHTML = ""; - return el.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( el ) { - el.innerHTML = ""; - el.firstChild.setAttribute( "value", "" ); - return el.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( el ) { - return el.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - null; - } - }); -} - -return Sizzle; - -})( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; - -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; -jQuery.escapeSelector = Sizzle.escape; - - - - -var dir = function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; -}; - - -var siblings = function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; -}; - - -var rneedsContext = jQuery.expr.match.needsContext; - - - -function nodeName( elem, name ) { - - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - -}; -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - - - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) !== not; - } ); - } - - // Single element - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - } ); - } - - // Arraylike of elements (jQuery, arguments, Array) - if ( typeof qualifier !== "string" ) { - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not; - } ); - } - - // Filtered directly for both simple and complex selectors - return jQuery.filter( qualifier, elements, not ); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - if ( elems.length === 1 && elem.nodeType === 1 ) { - return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; - } - - return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - } ) ); -}; - -jQuery.fn.extend( { - find: function( selector ) { - var i, ret, - len = this.length, - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter( function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - } ) ); - } - - ret = this.pushStack( [] ); - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - return len > 1 ? jQuery.uniqueSort( ret ) : ret; - }, - filter: function( selector ) { - return this.pushStack( winnow( this, selector || [], false ) ); - }, - not: function( selector ) { - return this.pushStack( winnow( this, selector || [], true ) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -} ); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - // Shortcut simple #id case for speed - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - - init = jQuery.fn.init = function( selector, context, root ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { - - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && ( match[ 1 ] || !context ) ) { - - // HANDLE: $(html) -> $(array) - if ( match[ 1 ] ) { - context = context instanceof jQuery ? context[ 0 ] : context; - - // Option to run scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - - // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[ 2 ] ); - - if ( elem ) { - - // Inject the element directly into the jQuery object - this[ 0 ] = elem; - this.length = 1; - } - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - - // Methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend( { - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter( function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[ i ] ) ) { - return true; - } - } - } ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - targets = typeof selectors !== "string" && jQuery( selectors ); - - // Positional selectors never match, since there's no _selection_ context - if ( !rneedsContext.test( selectors ) ) { - for ( ; i < l; i++ ) { - for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { - - // Always skip document fragments - if ( cur.nodeType < 11 && ( targets ? - targets.index( cur ) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector( cur, selectors ) ) ) { - - matched.push( cur ); - break; - } - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); - }, - - // Determine the position of an element within the set - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // Index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.uniqueSort( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - } -} ); - -function sibling( cur, dir ) { - while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each( { - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return siblings( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return siblings( elem.firstChild ); - }, - contents: function( elem ) { - if ( typeof elem.contentDocument !== "undefined" ) { - return elem.contentDocument; - } - - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } - - return jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.uniqueSort( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -} ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.stackTrace ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the stack, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getStackHook ) { - process.stackTrace = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the master Deferred - master = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - master.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( master.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return master.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); - } - - return master.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -jQuery.Deferred.exceptionHook = function( error, stack ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - - -// Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; - -// Used by camelCase as callback to replace() -function fcamelCase( all, letter ) { - return letter.toUpperCase(); -} - -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (#9572) -function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); -} -var acceptData = function( owner ) { - - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - - -function Data() { - this.expando = jQuery.expando + Data.uid++; -} - -Data.uid = 1; - -Data.prototype = { - - cache: function( owner ) { - - // Check if the owner object already has a cache - var value = owner[ this.expando ]; - - // If not, create one - if ( !value ) { - value = {}; - - // We can accept data for non-element nodes in modern browsers, - // but we should not, see #8335. - // Always return an empty object. - if ( acceptData( owner ) ) { - - // If it is a node unlikely to be stringify-ed or looped over - // use plain assignment - if ( owner.nodeType ) { - owner[ this.expando ] = value; - - // Otherwise secure it in a non-enumerable property - // configurable must be true to allow the property to be - // deleted when data is removed - } else { - Object.defineProperty( owner, this.expando, { - value: value, - configurable: true - } ); - } - } - } - - return value; - }, - set: function( owner, data, value ) { - var prop, - cache = this.cache( owner ); - - // Handle: [ owner, key, value ] args - // Always use camelCase key (gh-2257) - if ( typeof data === "string" ) { - cache[ camelCase( data ) ] = value; - - // Handle: [ owner, { properties } ] args - } else { - - // Copy the properties one-by-one to the cache object - for ( prop in data ) { - cache[ camelCase( prop ) ] = data[ prop ]; - } - } - return cache; - }, - get: function( owner, key ) { - return key === undefined ? - this.cache( owner ) : - - // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; - }, - access: function( owner, key, value ) { - - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ( ( key && typeof key === "string" ) && value === undefined ) ) { - - return this.get( owner, key ); - } - - // When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, - cache = owner[ this.expando ]; - - if ( cache === undefined ) { - return; - } - - if ( key !== undefined ) { - - // Support array or space separated string of keys - if ( Array.isArray( key ) ) { - - // If key is an array of keys... - // We always set camelCase keys, so remove that. - key = key.map( camelCase ); - } else { - key = camelCase( key ); - - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - key = key in cache ? - [ key ] : - ( key.match( rnothtmlwhite ) || [] ); - } - - i = key.length; - - while ( i-- ) { - delete cache[ key[ i ] ]; - } - } - - // Remove the expando if there's no more data - if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - - // Support: Chrome <=35 - 45 - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) - if ( owner.nodeType ) { - owner[ this.expando ] = undefined; - } else { - delete owner[ this.expando ]; - } - } - }, - hasData: function( owner ) { - var cache = owner[ this.expando ]; - return cache !== undefined && !jQuery.isEmptyObject( cache ); - } -}; -var dataPriv = new Data(); - -var dataUser = new Data(); - - - -// Implementation Summary -// -// 1. Enforce API surface and semantic compatibility with 1.9.x branch -// 2. Improve the module's maintainability by reducing the storage -// paths to a single mechanism. -// 3. Use the same single mechanism to support "private" and "user" data. -// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) -// 5. Avoid exposing implementation details on user objects (eg. expando properties) -// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /[A-Z]/g; - -function getData( data ) { - if ( data === "true" ) { - return true; - } - - if ( data === "false" ) { - return false; - } - - if ( data === "null" ) { - return null; - } - - // Only convert to a number if it doesn't change the string - if ( data === +data + "" ) { - return +data; - } - - if ( rbrace.test( data ) ) { - return JSON.parse( data ); - } - - return data; -} - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = getData( data ); - } catch ( e ) {} - - // Make sure we set the data so it isn't changed later - dataUser.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend( { - hasData: function( elem ) { - return dataUser.hasData( elem ) || dataPriv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return dataUser.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - dataUser.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to dataPriv methods, these can be deprecated. - _data: function( elem, name, data ) { - return dataPriv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - dataPriv.remove( elem, name ); - } -} ); - -jQuery.fn.extend( { - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = dataUser.get( elem ); - - if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE 11 only - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = camelCase( name.slice( 5 ) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - dataPriv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each( function() { - dataUser.set( this, key ); - } ); - } - - return access( this, function( value ) { - var data; - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - - // Attempt to get data from the cache - // The key will always be camelCased in Data - data = dataUser.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, key ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each( function() { - - // We always store the camelCased key - dataUser.set( this, key, value ); - } ); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each( function() { - dataUser.remove( this, key ); - } ); - } -} ); - - -jQuery.extend( { - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); - } -} ); - -jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } - - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); - - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -} ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - -var swap = function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} - -jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - } ); - } -} ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); - -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); - -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); - - - -// We have to close these tags to support XHTML (#13200) -var wrapMap = { - - // Support: IE <=9 only - option: [ 1, "" ], - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting or other required elements. - thead: [ 1, "", "
" ], - col: [ 2, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - _default: [ 0, "", "" ] -}; - -// Support: IE <=9 only -wrapMap.optgroup = wrapMap.option; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (#15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (#12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (#11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (#14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; -} )(); - - -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE <=9 - 11+ -// focus() and blur() are asynchronous, except when they are no-op. -// So expect focus to be synchronous when the element is already active, -// and blur to be synchronous when the element is not already active. -// (focus and blur are always synchronous in other supported browsers, -// this just defines when we can count on it). -function expectSync( elem, type ) { - return ( elem === safeActiveElement() ) === ( type === "focus" ); -} - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = {}; - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - // Make a writable jQuery.Event from the native event object - var event = jQuery.event.fix( nativeEvent ); - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", returnTrue ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, expectSync ) { - - // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add - if ( !expectSync ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var notAsync, result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - // Saved data should be false in such cases, but might be a leftover capture object - // from an async native handler (gh-4350) - if ( !saved.length ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - // Support: IE <=9 - 11+ - // focus() and blur() are asynchronous - notAsync = expectSync( this, type ); - this[ type ](); - result = dataPriv.get( this, type ); - if ( saved !== result || notAsync ) { - dataPriv.set( this, type, false ); - } else { - result = {}; - } - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - return result.value; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering the - // native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved.length ) { - - // ...and capture the result - dataPriv.set( this, type, { - value: jQuery.event.trigger( - - // Support: IE <=9 - 11+ - // Extend with the prototype to reset the above stopImmediatePropagation() - jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), - saved.slice( 1 ), - this - ) - } ); - - // Abort handling of the native event - event.stopImmediatePropagation(); - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (#504, #13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - - which: function( event ) { - var button = event.button; - - // Add which for key events - if ( event.which == null && rkeyEvent.test( event.type ) ) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { - if ( button & 1 ) { - return 1; - } - - if ( button & 2 ) { - return 3; - } - - if ( button & 4 ) { - return 2; - } - - return 0; - } - - return event.which; - } -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, expectSync ); - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - delegateType: delegateType - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - /* eslint-disable max-len */ - - // See https://github.com/eslint/eslint/issues/3229 - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, - - /* eslint-enable */ - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.access( src ); - pdataCur = dataPriv.set( dest, pdataOld ); - events = pdataOld.events; - - if ( events ) { - delete pdataCur.handle; - pdataCur.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - } ); - } - } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html.replace( rxhtmlTag, "<$1>" ); - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (#8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, #12537) - // .css('--customProperty) (#3144) - if ( computed ) { - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - if ( box === "margin" ) { - delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - // Support: IE 9-11 only - // Also use offsetWidth/offsetHeight for when box sizing is unreliable - // We use getClientRects() to check for hidden/disconnected. - // In those cases, the computed value can be trusted to be border-box - if ( ( !support.boxSizingReliable() && isBorderBox || - val === "auto" || - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "animationIterationCount": true, - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "gridArea": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnStart": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowStart": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (#7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (#7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || - tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, inProgress, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function schedule() { - if ( inProgress ) { - if ( document.hidden === false && window.requestAnimationFrame ) { - window.requestAnimationFrame( schedule ); - } else { - window.setTimeout( schedule, jQuery.fx.interval ); - } - - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = Date.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 15 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY and Edge just mirrors - // the overflowX value there. - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* eslint-disable no-loop-func */ - - anim.done( function() { - - /* eslint-enable no-loop-func */ - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( Array.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - // If there's more to do, yield - if ( percent < 1 && length ) { - return remaining; - } - - // If this was an empty animation, synthesize a final progress notification - if ( !length ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - } - - // Resolve the animation and report its conclusion - deferred.resolveWith( elem, [ animation ] ); - return false; - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - result.stop.bind( result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - // Attach callbacks from options - animation - .progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - return animation; -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnothtmlwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !isFunction( easing ) && easing - }; - - // Go to the end state if fx are off - if ( jQuery.fx.off ) { - opt.duration = 0; - - } else { - if ( typeof opt.duration !== "number" ) { - if ( opt.duration in jQuery.fx.speeds ) { - opt.duration = jQuery.fx.speeds[ opt.duration ]; - - } else { - opt.duration = jQuery.fx.speeds._default; - } - } - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue && type !== false ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = Date.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Run the timer and safely remove it when done (allowing for external removal) - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - jQuery.fx.start(); -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( inProgress ) { - return; - } - - inProgress = true; - schedule(); -}; - -jQuery.fx.stop = function() { - inProgress = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) > -1 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - return this.each( function() { - var className, i, self, classNames; - - if ( isValidValue ) { - - // Toggle individual class names - i = 0; - self = jQuery( this ); - classNames = classesToArray( value ); - - while ( ( className = classNames[ i++ ] ) ) { - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (#14686, #14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -support.focusin = "onfocusin" in window; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -// Support: Firefox <=44 -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = dataPriv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = dataPriv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - dataPriv.remove( doc, fix ); - - } else { - dataPriv.access( doc, fix, attaches ); - } - } - }; - } ); -} -var location = window.location; - -var nonce = Date.now(); - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) { - xml = undefined; - } - - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; -}; - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ) - .filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ) - .map( function( i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rantiCache = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - - if ( isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() + " " ] = - ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) - .concat( match[ 2 ] ); - } - } - match = responseHeaders[ key.toLowerCase() + " " ]; - } - return match == null ? null : match.join( ", " ); - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 15 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available and should be processed, append data to url - if ( s.data && ( s.processData || typeof s.data === "string" ) ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add or update anti-cache param if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - - -jQuery._evalUrl = function( url, options ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (#11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - - // Only evaluate the response if it is successful (gh-4126) - // dataFilter is not invoked for failure responses, so using it instead - // of the default converter is kludgy but it works. - converters: { - "text script": function() {} - }, - dataFilter: function( response ) { - jQuery.globalEval( response, options ); - } - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // #1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.ontimeout = - xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see #8605, #14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // #14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( " - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

API Reference¶

-

These pages were automatically generated from docstrings in code.

-

They might be outdated, or incomplete.

-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/creatingacommand.html b/docs/html/creatingacommand.html deleted file mode 100644 index ddd25f35..00000000 --- a/docs/html/creatingacommand.html +++ /dev/null @@ -1,575 +0,0 @@ - - - - - - - - - - - Royalnet Commands — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

Royalnet Commands¶

-

A Royalnet Command is a small script that is run whenever a specific message is sent to a Royalnet interface.

-

A Command code looks like this:

-
from royalnet.commands import Command
-
-class PingCommand(Command):
-    name = "ping"
-
-    description = "Play ping-pong with the bot."
-
-    def __init__(self, interface):
-        # This code is run just once, while the bot is starting
-        super().__init__()
-
-    async def run(self, args, data):
-        # This code is run every time the command is called
-        await data.reply("Pong!")
-
-
-
-

Creating a new Command¶

-

First, think of a name for your command. -It’s the name your command will be called with: for example, the “spaghetti†command will be called by typing /spaghetti in chat. -Try to keep the name as short as possible, while staying specific enough so no other command will have the same name.

-

Next, create a new Python file with the name you have thought of. -The previously mentioned “spaghetti†command should have a file called spaghetti.py.

-

Then, in the first row of the file, import the Command class from royalnet, and create a new class inheriting from it:

-
from royalnet.commands import Command
-
-class SpaghettiCommand(Command):
-    ...
-
-
-

Inside the class, override the attributes name and description with respectively the name of the command and a small description of what the command will do:

-
from royalnet.commands import Command
-
-class SpaghettiCommand(Command):
-    name = "spaghetti"
-
-    description = "Send a spaghetti emoji in the chat."
-
-
-

Now override the Command.run() method, adding the code you want the bot to run when the command is called.

-

To send a message in the chat the command was called in, you can use the CommandData.reply() method:

-
from royalnet.commands import Command
-
-class SpaghettiCommand(Command):
-    name = "spaghetti"
-
-    description = "Send a spaghetti emoji in the chat."
-
-    async def run(self, args, data):
-        await data.reply("ðŸ")
-
-
-

And… it’s done! The command is ready to be added to a Pack!

-
-
-

Command arguments¶

-

A command can have some arguments passed by the user: for example, on Telegram an user may type /spaghetti carbonara al-dente -to pass the str “carbonara al-dente†to the command code.

-

These arguments can be accessed in multiple ways through the args parameter passed to the Command.run() -method.

-

If you want your command to use arguments, override the syntax class attribute with a brief description of the -syntax of your command, possibly using {curly braces} for required arguments and [square brackets] for optional -ones.

-
from royalnet.commands import Command
-
-class SpaghettiCommand(Command):
-    name = "spaghetti"
-
-    description = "Send a spaghetti emoji in the chat."
-
-    syntax = "(requestedpasta)"
-
-    async def run(self, args, data):
-        await data.reply(f"ðŸ Here's your {args[0]}!")
-
-
-
-

Direct access¶

-

You can consider arguments as if they were separated by spaces.

-

You can then access command arguments directly by number as if the args object was a list of str.

-

If you request an argument with a certain number, but the argument does not exist, an -royalnet.error.InvalidInputError is raised, making the arguments accessed in this way required.

-
args[0]
-# "carbonara"
-
-args[1]
-# "al-dente"
-
-args[2]
-# InvalidInputError() is raised
-
-
-
-
-

Optional access¶

-

If you don’t want arguments to be required, you can access them through the CommandArgs.optional() method: it -will return None if the argument wasn’t passed, making it optional.

-
args.optional(0)
-# "carbonara"
-
-args.optional(1)
-# "al-dente"
-
-args.optional(2)
-# None
-
-
-

You can specify a default result too, so that the method will return it instead of returning None:

-
args.optional(2, default="banana")
-# "banana"
-
-
-
-
-

Full string¶

-

If you want the full argument string, you can use the CommandArgs.joined() method.

-
args.joined()
-# "carbonara al-dente"
-
-
-

You can specify a minimum number of arguments too, so that an InvalidInputError will be -raised if not enough arguments are present:

-
args.joined(require_at_least=3)
-# InvalidInputError() is raised
-
-
-
-
-

Regular expressions¶

-

For more complex commands, you may want to get arguments through regular expressions.

-

You can then use the CommandArgs.match() method, which tries to match a pattern to the command argument string, -which returns a tuple of the matched groups and raises an InvalidInputError if there is no match.

-

To match a pattern, re.match() is used, meaning that Python will try to match only at the beginning of the string.

-
args.match(r"(carb\w+)")
-# ("carbonara",)
-
-args.match(r"(al-\w+)")
-# InvalidInputError() is raised
-
-args.match(r"\s*(al-\w+)")
-# ("al-dente",)
-
-args.match(r"\s*(carb\w+)\s*(al-\w+)")
-# ("carbonara", "al-dente")
-
-
-
-
-
-

Raising errors¶

-

If you want to display an error message to the user, you can raise a CommandError using the error message as argument:

-
if not kitchen.is_open():
-    raise CommandError("The kitchen is closed. Come back later!")
-
-
-

You can also manually raise InvalidInputError to redisplay the command syntax, along with your error message:

-
if args[0] not in allowed_pasta:
-    raise InvalidInputError("The specified pasta type is invalid.")
-
-
-

If you need a Royalnet feature that’s not available on the current interface, you can raise an -UnsupportedError with a brief description of what’s missing:

-
if interface.name != "telegram":
-    raise UnsupportedError("This command can only be run on Telegram interfaces.")
-
-
-
-
-

Running code at the initialization of the bot¶

-

You can run code while the bot is starting by overriding the Command.__init__() function.

-

You should keep the super().__init__(interface) call at the start of it, so that the Command instance is -initialized properly, then add your code after it.

-

You can add fields to the command to keep shared data between multiple command calls (but not bot restarts): it may -be useful for fetching external static data and keeping it until the bot is restarted, or to store references to all the -asyncio.Task started by the bot.

-
from royalnet.commands import Command
-
-class SpaghettiCommand(Command):
-    name = "spaghetti"
-
-    description = "Send a spaghetti emoji in the chat."
-
-    syntax = "{pasta}"
-
-    def __init__(self, interface):
-        super().__init__(interface)
-        self.requested_pasta = []
-
-    async def run(self, args, data):
-        pasta = args[0]
-        if pasta in self.requested_pasta:
-            await data.reply(f"âš ï¸ This pasta was already requested before.")
-            return
-        self.requested_pasta.append(pasta)
-        await data.reply(f"ðŸ Here's your {pasta}!")
-
-
-
-
-

Coroutines and slow operations¶

-

You may have noticed that in the previous examples we used await data.reply("ðŸ") instead of just data.reply("ðŸ").

-

This is because CommandData.reply() isn’t a simple method: it is a coroutine, a special kind of function that -can be executed separately from the rest of the code, allowing the bot to do other things in the meantime.

-

By adding the await keyword before the data.reply("ðŸ"), we tell the bot that it can do other things, like -receiving new messages, while the message is being sent.

-

You should avoid running slow normal functions inside bot commands, as they will stop the bot from working until they -are finished and may cause bugs in other parts of the code!

-
async def run(self, args, data):
-    # Don't do this!
-    image = download_1_terabyte_of_spaghetti("right_now", from="italy")
-    ...
-
-
-

If the slow function you want does not cause any side effect, you can wrap it with the royalnet.utils.asyncify() -function:

-
async def run(self, args, data):
-    # If the called function has no side effect, you can do this!
-    image = await asyncify(download_1_terabyte_of_spaghetti, "right_now", from="italy")
-    ...
-
-
-

Avoid using time.sleep() function, as it is considered a slow operation: use instead asyncio.sleep(), -a coroutine that does the same exact thing.

-
-
-

Delete the invoking message¶

-

The invoking message of a command is the message that the user sent that the bot recognized as a command; for example, -the message /spaghetti carbonara is the invoking message for the spaghetti command run.

-

You can have the bot delete the invoking message for a command by calling the CommandData.delete_invoking -method:

-
async def run(self, args, data):
-    await data.delete_invoking()
-
-
-

Not all interfaces support deleting messages; by default, if the interface does not support deletions, the call is -ignored.

-

You can have the method raise an error if the message can’t be deleted by setting the error_if_unavailable parameter -to True:

-
async def run(self, args, data):
-    try:
-        await data.delete_invoking(error_if_unavailable=True)
-    except royalnet.error.UnsupportedError:
-        await data.reply("🚫 The message could not be deleted.")
-    else:
-        await data.reply("✅ The message was deleted!")
-
-
-
-
-

Using the database¶

-

Bots can be connected to a PostgreSQL database through a special SQLAlchemy interface called -royalnet.database.Alchemy.

-

If the connection is established, the self.alchemy and data.session fields will be -available for use in commands.

-

self.interface.alchemy is an instance of royalnet.database.Alchemy, which contains the -sqlalchemy.engine.Engine, metadata and tables, while data.session is a -sqlalchemy.orm.session.Session, and can be interacted in the same way as one.

-

If you want to use royalnet.database.Alchemy in your command, you should override the -tables field with the set of Alchemy tables you need.

-
from royalnet.commands import Command
-from royalnet.packs.common.tables import User
-
-class SpaghettiCommand(Command):
-    name = "spaghetti"
-
-    description = "Send a spaghetti emoji in the chat."
-
-    syntax = "{pasta}"
-
-    tables = {User}
-
-    ...
-
-
-
-

Querying the database¶

-

You can sqlalchemy.orm.query.Query the database using the SQLAlchemy ORM.

-

The SQLAlchemy tables can be found inside royalnet.database.Alchemy with the same name they were created -from, if they were specified in tables.

-
query = data.session.query(User)
-
-
-
-
-

Adding filters to the query¶

-

You can filter the query results with the sqlalchemy.orm.query.Query.filter() method.

-
-

Note

-

Remember to always use a table column as first comparision element, as it won’t work otherwise.

-
-
query = query.filter(User.role == "Member")
-
-
-
-
-

Ordering the results of a query¶

-

You can order the query results in ascending order with the sqlalchemy.orm.query.Query.order_by() method.

-
query = query.order_by(User.username)
-
-
-

Additionally, you can append the .desc() method to a table column to sort in descending order:

-
query = query.order_by(User.username.desc())
-
-
-
-
-

Fetching the results of a query¶

-

You can fetch the query results with the sqlalchemy.orm.query.Query.all(), -sqlalchemy.orm.query.Query.first(), sqlalchemy.orm.query.Query.one() and -sqlalchemy.orm.query.Query.one_or_none() methods.

-

Remember to use royalnet.utils.asyncify() when fetching results, as it may take a while!

-

Use sqlalchemy.orm.query.Query.all() if you want a list of all results:

-
results: list = await asyncify(query.all)
-
-
-

Use sqlalchemy.orm.query.Query.first() if you want the first result of the list, or None if -there are no results:

-
result: typing.Union[..., None] = await asyncify(query.first)
-
-
-

Use sqlalchemy.orm.query.Query.one() if you expect to have a single result, and you want the command to -raise an error if any different number of results is returned:

-
result: ... = await asyncify(query.one)  # Raises an error if there are no results or more than a result.
-
-
-

Use sqlalchemy.orm.query.Query.one_or_none() if you expect to have a single result, or nothing, and -if you want the command to raise an error if the number of results is greater than one.

-
result: typing.Union[..., None] = await asyncify(query.one_or_none)  # Raises an error if there is more than a result.
-
-
-
-
-

More Alchemy¶

-

You can read more about sqlalchemy at their website.

-
-
-
-

Comunicating via Royalnet¶

-

This section is not documented yet.

-
-
-

Adding the command to a Pack¶

-

This section is not documented yet.

-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/genindex.html b/docs/html/genindex.html deleted file mode 100644 index cb0d23c6..00000000 --- a/docs/html/genindex.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - Index — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Index
  • - - -
  • - - - -
  • - -
- - -
-
-
-
- - -

Index

- -
- R - -
-

R

- - -
- - - -
- -
-
- - -
- -
-

- © Copyright 2019, Stefano Pigozzi - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/index.html b/docs/html/index.html deleted file mode 100644 index d4a06f8a..00000000 --- a/docs/html/index.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - - - - - - royalnet — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
- - - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/objects.inv b/docs/html/objects.inv deleted file mode 100644 index 7fb462af..00000000 --- a/docs/html/objects.inv +++ /dev/null @@ -1,5 +0,0 @@ -# Sphinx inventory version 2 -# Project: Royalnet -# Version: -# The remainder of this file is compressed using zlib. -xÚm±Â0 D÷|…%Xƒ`톘: Uð&1i¥$­ÒT¢O‹…-ºó»³Ú­§ÝX¸V–`Ø5îÈ+ÚÕÑÙ [r Rä&ôQºU ¿«.I*ÆÆT­sèuή=æ/i·«½0ä¯éñb-ÞÈÎtR™*ç§øŒ-ÙDXrÅtÕŸ¸n”Éàù3ÿ ‡ÁOm&eä+k9‚Å÷1¢' ªþ®d+ûOLJ•l \ No newline at end of file diff --git a/docs/html/py-modindex.html b/docs/html/py-modindex.html deleted file mode 100644 index b9119d79..00000000 --- a/docs/html/py-modindex.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - - - Python Module Index — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Python Module Index
  • - - -
  • - -
  • - -
- - -
-
-
-
- - -

Python Module Index

- -
- r -
- - - - - - - -
 
- r
- royalnet -
- - -
- -
-
- - -
- -
-

- © Copyright 2019, Stefano Pigozzi - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/runningroyalnet.html b/docs/html/runningroyalnet.html deleted file mode 100644 index 4e1840ef..00000000 --- a/docs/html/runningroyalnet.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - - - - - - - Running Royalnet — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

Running Royalnet¶

-

To run a royalnet instance, you have first to download the package from pip:

-
-

The Keyring¶

-
pip install royalnet
-
-
-

To run royalnet, you’ll have to setup the system keyring.

-

On Windows and desktop Linux, this is already configured; -on a headless Linux instance, you’ll need to manually start and unlock the keyring daemon.

-

Now you have to create a new royalnet configuration. Start the configuration wizard:

-
python -m royalnet.configurator
-
-
-

You’ll be prompted to enter a “secrets nameâ€: this is the name of the group of API keys that will be associated with -your bot. Enter a name that you’ll be able to remember.

-
Desired secrets name [__default__]: royalgames
-
-
-

You’ll then be asked for a network password.

-

This password is used to connect to the rest of the royalnet.network, or, if you’re hosting a local Network, -it will be the necessary password to connect to it:

-
Network password []: cosafaunapesuunafoglia
-
-
-

Then you’ll be asked for a Telegram Bot API token. -You can get one from @BotFather.

-
Telegram Bot API token []: 000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-
-
-

The next prompt will ask for a Discord Bot API token. -You can get one at the Discord Developers Portal.

-
Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA
-
-
-

Now the configurator will ask you for a Imgur API token. -Register an application on Imgur to be supplied one. -The token should be of type “anonymous usage without user authorizationâ€.

-
Imgur API token []: aaaaaaaaaaaaaaa
-
-
-

Next, you’ll be asked for a Sentry DSN. You probably won’t have one, so just ignore it and press enter.

-
Sentry DSN []:
-
-
-

Now that all tokens are configured, you’re ready to launch the bot!

-
-
-

Running the bots¶

-

You can run the main royalnet process by running:

-
python3.7 -m royalnet
-
-
-

To see all available options, you can run:

-
python3.7 -m royalnet --help
-
-
-
-

Note

-

All royalnet options should be specified after the word royalnet, or else they will be passed to -the Python interpreter.

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/search.html b/docs/html/search.html deleted file mode 100644 index 3bb00438..00000000 --- a/docs/html/search.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - - - - - Search — Royalnet documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • Docs »
  • - -
  • Search
  • - - -
  • - - - -
  • - -
- - -
-
-
-
- - - - -
- -
- -
- -
-
- - -
- -
-

- © Copyright 2019, Stefano Pigozzi - -

-
- Built with Sphinx using a theme provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/html/searchindex.js b/docs/html/searchindex.js deleted file mode 100644 index b159495b..00000000 --- a/docs/html/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({docnames:["apireference","creatingacommand","index","runningroyalnet"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["apireference.rst","creatingacommand.rst","index.rst","runningroyalnet.rst"],objects:{"":{royalnet:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"]},objtypes:{"0":"py:module"},terms:{"class":1,"default":1,"function":1,"import":1,"new":[2,3],"return":1,"short":1,"static":1,"super":1,"true":1,"try":1,"while":1,Adding:2,And:1,For:1,Not:1,The:[1,2],Then:[1,3],These:[0,1],Use:1,Using:2,__default__:3,__init__:1,aaaaaa:3,aaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:3,abl:3,about:1,access:2,add:1,added:1,adding:1,addition:1,after:[1,3],alchemi:2,all:[1,3],allow:1,allowed_pasta:1,along:1,alreadi:[1,3],also:1,alwai:1,ani:1,anonym:3,api:[2,3],append:1,applic:3,arg:1,argument:2,ascend:1,ask:3,associ:3,async:1,asyncifi:1,asyncio:1,attribut:1,author:3,automat:0,avail:[1,3],avoid:1,await:1,back:1,banana:1,becaus:1,befor:1,begin:1,being:1,between:1,bot:2,botfath:3,brace:1,bracket:1,brief:1,bug:1,call:1,can:[1,3],carb:1,carbonara:1,caus:1,certain:1,chat:1,close:1,code:[0,2],column:1,come:1,command:2,commandarg:1,commanddata:1,commanderror:1,common:1,comparis:1,complex:1,comun:2,configur:3,connect:[1,3],consid:1,contain:1,coroutin:2,cosafaunapesuunafoglia:3,could:1,creat:[2,3],curli:1,current:1,daemon:3,data:1,databas:2,def:1,delet:2,delete_invok:1,dent:1,desc:1,descend:1,descript:1,desir:3,desktop:3,develop:3,differ:1,direct:2,directli:1,discord:3,displai:1,docstr:0,document:[1,2],doe:1,don:1,done:1,download:3,download_1_terabyte_of_spaghetti:1,dsn:3,effect:1,element:1,els:[1,3],emoji:1,engin:1,enough:1,enter:3,error:2,error_if_unavail:1,establish:1,everi:1,exact:1,exampl:1,except:1,execut:1,exist:1,expect:1,express:2,extern:1,featur:1,fetch:2,field:1,file:1,filter:2,finish:1,first:[1,3],found:1,from:[0,1,3],full:2,gener:0,get:[1,3],github:2,greater:1,group:[1,3],has:1,have:[1,3],headless:3,help:3,here:1,host:3,ignor:[1,3],imag:1,imgur:3,incomplet:0,index:2,inherit:1,initi:2,insid:1,instal:3,instanc:[1,3],instead:1,interact:1,interfac:1,interpret:3,invalid:1,invalidinputerror:1,invok:2,is_open:1,isn:1,itali:1,join:1,just:[1,3],keep:1,kei:3,keyr:2,keyword:1,kind:1,kitchen:1,later:1,launch:3,like:1,linux:3,list:1,local:3,look:1,mai:1,main:3,make:1,manual:[1,3],match:1,mean:1,meantim:1,member:1,mention:1,messag:2,metadata:1,method:1,might:0,minimum:1,miss:1,more:2,multipl:1,name:[1,3],necessari:3,need:[1,3],network:3,next:[1,3],none:1,normal:1,noth:1,notic:1,now:[1,3],number:1,object:1,onc:1,one:[1,3],one_or_non:1,ones:1,onli:1,oper:2,option:[2,3],order:2,order_bi:1,orm:1,other:1,otherwis:1,outdat:0,overrid:1,pack:2,packag:3,page:0,paramet:1,part:1,pass:[1,3],password:3,pasta:1,pattern:1,ping:1,pingcommand:1,pip:3,plai:1,pong:1,portal:3,possibl:1,postgresql:1,present:1,press:3,previou:1,previous:1,probabl:3,process:3,prompt:3,properli:1,python3:3,python:[1,3],queri:2,rais:2,read:1,readi:[1,3],receiv:1,recogn:1,redisplai:1,refer:[1,2],regist:3,regular:2,rememb:[1,3],repli:1,request:1,requested_pasta:1,requestedpasta:1,requir:1,require_at_least:1,respect:1,rest:[1,3],restart:1,result:2,right_now:1,role:1,row:1,royalgam:3,run:2,same:1,script:1,secret:3,section:1,see:3,self:1,send:1,sent:1,sentri:3,separ:1,session:1,set:1,setup:3,share:1,should:[1,3],side:1,simpl:1,singl:1,sleep:1,slow:2,small:1,some:1,sort:1,space:1,spaghetti:1,spaghetticommand:1,special:1,specif:1,specifi:[1,3],sqlalchemi:1,squar:1,stai:1,start:[1,3],stop:1,store:1,str:1,string:2,suppli:3,support:1,syntax:1,system:3,tabl:1,take:1,task:1,telegram:[1,3],tell:1,than:1,thei:[0,1,3],them:1,thi:[1,3],thing:1,think:1,thought:1,through:1,time:1,token:3,too:1,tri:1,tupl:1,type:[1,3],union:1,unlock:3,unsupportederror:1,until:1,usag:3,use:1,used:[1,3],useful:1,user:[1,3],usernam:1,using:1,util:1,via:2,wai:1,want:1,wasn:1,websit:1,welcom:2,were:[0,1],what:1,when:1,whenev:1,which:1,window:3,without:3,wizard:3,won:[1,3],word:3,work:1,wrap:1,yet:1,you:[1,3],your:[1,3]},titles:["API Reference","Royalnet Commands","royalnet","Running Royalnet"],titleterms:{"new":1,Adding:1,The:3,Using:1,access:1,alchemi:1,api:0,argument:1,bot:[1,3],code:1,command:1,comun:1,coroutin:1,creat:1,databas:1,delet:1,direct:1,error:1,express:1,fetch:1,filter:1,full:1,initi:1,invok:1,keyr:3,link:2,messag:1,more:1,oper:1,option:1,order:1,pack:1,queri:1,rais:1,refer:0,regular:1,result:1,royalnet:[1,2,3],run:[1,3],slow:1,some:2,string:1,useful:2,via:1}}) \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 2e11dadb..00000000 --- a/docs/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/docs_source/apireference.rst b/docs_source/apireference.rst index a8b13455..b359643d 100644 --- a/docs_source/apireference.rst +++ b/docs_source/apireference.rst @@ -1,11 +1,63 @@ API Reference -==================================== +================ -These pages were automatically generated from docstrings in code. +This page is autogenerated from the docstrings inside the code. -They might be outdated, or incomplete. +.. currentmodule:: royalnet -.. automodule:: royalnet +Alchemy +---------------- + +.. automodule:: royalnet.alchemy :members: :undoc-members: - :private-members: + +Backpack +---------------- + +.. automodule:: royalnet.backpack + :members: + :undoc-members: + +Bard +---------------- + +.. automodule:: royalnet.bard + :members: + :undoc-members: + +Commands +---------------- + +.. automodule:: royalnet.commands + :members: + :undoc-members: + +Constellation +---------------- + +.. automodule:: royalnet.constellation + :members: + :undoc-members: + +Herald +---------------- + +.. automodule:: royalnet.herald + :members: + :undoc-members: + +Serf +---------------- + +.. automodule:: royalnet.serf + :members: + :undoc-members: + +Utils +---------------- + +.. automodule:: royalnet.utils + :members: + :undoc-members: + diff --git a/docs_source/conf.py b/docs_source/conf.py index 39564322..b118d47e 100644 --- a/docs_source/conf.py +++ b/docs_source/conf.py @@ -14,24 +14,37 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +from royalnet import __version__ as royalnet_version # -- Project information ----------------------------------------------------- project = 'Royalnet' copyright = '2019, Stefano Pigozzi' author = 'Stefano Pigozzi' +version = royalnet_version +release = royalnet_version # -- General configuration --------------------------------------------------- # 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.napoleon", "sphinx.ext.intersphinx"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", +] -intersphinx_mapping = {"python": ("https://docs.python.org/3.7", None), - "discord": ("https://discordpy.readthedocs.io/en/latest/", None), - "telegram": ("https://python-telegram-bot.readthedocs.io/en/stable/", None), - "sqlalchemy": ("https://docs.sqlalchemy.org/en/13/", None)} +intersphinx_mapping = { + "python": ("https://docs.python.org/3.8", None), + "keyring": ("https://keyring.readthedocs.io/en/latest/", None), + "telegram": ("https://python-telegram-bot.readthedocs.io/en/stable/", None), + "discord": ("https://discordpy.readthedocs.io/en/latest/", None), + "ffmpeg_python": ("https://kkroening.github.io/ffmpeg-python/", None), + "sqlalchemy": ("https://docs.sqlalchemy.org/en/13/", None), + "psycopg2": ("http://initd.org/psycopg/docs/", None), + "websockets": ("https://websockets.readthedocs.io/en/stable/", None), +} def skip(app, what, name: str, obj, would_skip, options): @@ -65,3 +78,7 @@ html_theme = 'sphinx_rtd_theme' # 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'] + +# Display warnings on the pages... for now. +keep_warnings = True +nitpicky = True \ No newline at end of file diff --git a/docs_source/creatingacommand.rst b/docs_source/creatingacommand.rst deleted file mode 100644 index 7f72cf7b..00000000 --- a/docs_source/creatingacommand.rst +++ /dev/null @@ -1,381 +0,0 @@ -.. currentmodule:: royalnet.commands - -Royalnet Commands -==================================== - -A Royalnet Command is a small script that is run whenever a specific message is sent to a Royalnet interface. - -A Command code looks like this: :: - - from royalnet.commands import Command - - class PingCommand(Command): - name = "ping" - - description = "Play ping-pong with the bot." - - def __init__(self, interface): - # This code is run just once, while the bot is starting - super().__init__() - - async def run(self, args, data): - # This code is run every time the command is called - await data.reply("Pong!") - -Creating a new Command ------------------------------------- - -First, think of a ``name`` for your command. -It's the name your command will be called with: for example, the "spaghetti" command will be called by typing **/spaghetti** in chat. -Try to keep the name as short as possible, while staying specific enough so no other command will have the same name. - -Next, create a new Python file with the ``name`` you have thought of. -The previously mentioned "spaghetti" command should have a file called ``spaghetti.py``. - -Then, in the first row of the file, import the :py:class:`Command` class from royalnet, and create a new class inheriting from it: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - ... - -Inside the class, override the attributes ``name`` and ``description`` with respectively the **name of the command** and a **small description of what the command will do**: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - -Now override the :py:meth:`Command.run` method, adding the code you want the bot to run when the command is called. - -To send a message in the chat the command was called in, you can use the :py:meth:`CommandData.reply` method: :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - async def run(self, args, data): - await data.reply("ðŸ") - -And... it's done! The command is ready to be added to a Pack! - -Command arguments ------------------------------------- - -A command can have some arguments passed by the user: for example, on Telegram an user may type `/spaghetti carbonara al-dente` -to pass the :py:class:`str` `"carbonara al-dente"` to the command code. - -These arguments can be accessed in multiple ways through the ``args`` parameter passed to the :py:meth:`Command.run` -method. - -If you want your command to use arguments, override the ``syntax`` class attribute with a brief description of the -syntax of your command, possibly using {curly braces} for required arguments and [square brackets] for optional -ones. :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "(requestedpasta)" - - async def run(self, args, data): - await data.reply(f"ðŸ Here's your {args[0]}!") - - -Direct access -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can consider arguments as if they were separated by spaces. - -You can then access command arguments directly by number as if the args object was a list of :py:class:`str`. - -If you request an argument with a certain number, but the argument does not exist, an -:py:exc:`royalnet.error.InvalidInputError` is raised, making the arguments accessed in this way **required**. :: - - args[0] - # "carbonara" - - args[1] - # "al-dente" - - args[2] - # InvalidInputError() is raised - -Optional access -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't want arguments to be required, you can access them through the :py:meth:`CommandArgs.optional` method: it -will return :py:const:`None` if the argument wasn't passed, making it **optional**. :: - - args.optional(0) - # "carbonara" - - args.optional(1) - # "al-dente" - - args.optional(2) - # None - -You can specify a default result too, so that the method will return it instead of returning :py:const:`None`: :: - - args.optional(2, default="banana") - # "banana" - -Full string -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want the full argument string, you can use the :py:meth:`CommandArgs.joined` method. :: - - args.joined() - # "carbonara al-dente" - -You can specify a minimum number of arguments too, so that an :py:exc:`InvalidInputError` will be -raised if not enough arguments are present: :: - - args.joined(require_at_least=3) - # InvalidInputError() is raised - -Regular expressions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For more complex commands, you may want to get arguments through `regular expressions `_. - -You can then use the :py:meth:`CommandArgs.match` method, which tries to match a pattern to the command argument string, -which returns a tuple of the matched groups and raises an :py:exc:`InvalidInputError` if there is no match. - -To match a pattern, :py:func:`re.match` is used, meaning that Python will try to match only at the beginning of the string. :: - - args.match(r"(carb\w+)") - # ("carbonara",) - - args.match(r"(al-\w+)") - # InvalidInputError() is raised - - args.match(r"\s*(al-\w+)") - # ("al-dente",) - - args.match(r"\s*(carb\w+)\s*(al-\w+)") - # ("carbonara", "al-dente") - -Raising errors ---------------------------------------------- - -If you want to display an error message to the user, you can raise a :py:exc:`CommandError` using the error message as argument: :: - - if not kitchen.is_open(): - raise CommandError("The kitchen is closed. Come back later!") - -You can also manually raise :py:exc:`InvalidInputError` to redisplay the command syntax, along with your error message: :: - - if args[0] not in allowed_pasta: - raise InvalidInputError("The specified pasta type is invalid.") - -If you need a Royalnet feature that's not available on the current interface, you can raise an -:py:exc:`UnsupportedError` with a brief description of what's missing: :: - - if interface.name != "telegram": - raise UnsupportedError("This command can only be run on Telegram interfaces.") - -Running code at the initialization of the bot ---------------------------------------------- - -You can run code while the bot is starting by overriding the :py:meth:`Command.__init__` function. - -You should keep the ``super().__init__(interface)`` call at the start of it, so that the :py:class:`Command` instance is -initialized properly, then add your code after it. - -You can add fields to the command to keep **shared data between multiple command calls** (but not bot restarts): it may -be useful for fetching external static data and keeping it until the bot is restarted, or to store references to all the -:py:class:`asyncio.Task` started by the bot. :: - - from royalnet.commands import Command - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "{pasta}" - - def __init__(self, interface): - super().__init__(interface) - self.requested_pasta = [] - - async def run(self, args, data): - pasta = args[0] - if pasta in self.requested_pasta: - await data.reply(f"âš ï¸ This pasta was already requested before.") - return - self.requested_pasta.append(pasta) - await data.reply(f"ðŸ Here's your {pasta}!") - - -Coroutines and slow operations ------------------------------------- - -You may have noticed that in the previous examples we used ``await data.reply("ðŸ")`` instead of just ``data.reply("ðŸ")``. - -This is because :py:meth:`CommandData.reply` isn't a simple method: it is a coroutine, a special kind of function that -can be executed separately from the rest of the code, allowing the bot to do other things in the meantime. - -By adding the ``await`` keyword before the ``data.reply("ðŸ")``, we tell the bot that it can do other things, like -receiving new messages, while the message is being sent. - -You should avoid running slow normal functions inside bot commands, as they will stop the bot from working until they -are finished and may cause bugs in other parts of the code! :: - - async def run(self, args, data): - # Don't do this! - image = download_1_terabyte_of_spaghetti("right_now", from="italy") - ... - -If the slow function you want does not cause any side effect, you can wrap it with the :py:func:`royalnet.utils.asyncify` -function: :: - - async def run(self, args, data): - # If the called function has no side effect, you can do this! - image = await asyncify(download_1_terabyte_of_spaghetti, "right_now", from="italy") - ... - -Avoid using :py:func:`time.sleep` function, as it is considered a slow operation: use instead :py:func:`asyncio.sleep`, -a coroutine that does the same exact thing. - -Delete the invoking message ------------------------------------- - -The invoking message of a command is the message that the user sent that the bot recognized as a command; for example, -the message ``/spaghetti carbonara`` is the invoking message for the ``spaghetti`` command run. - -You can have the bot delete the invoking message for a command by calling the :py:class:`CommandData.delete_invoking` -method: :: - - async def run(self, args, data): - await data.delete_invoking() - -Not all interfaces support deleting messages; by default, if the interface does not support deletions, the call is -ignored. - -You can have the method raise an error if the message can't be deleted by setting the ``error_if_unavailable`` parameter -to True: :: - - async def run(self, args, data): - try: - await data.delete_invoking(error_if_unavailable=True) - except royalnet.error.UnsupportedError: - await data.reply("🚫 The message could not be deleted.") - else: - await data.reply("✅ The message was deleted!") - -Using the database ------------------------------------- - -Bots can be connected to a PostgreSQL database through a special SQLAlchemy interface called -:py:class:`royalnet.database.Alchemy`. - -If the connection is established, the ``self.alchemy`` and ``data.session`` fields will be -available for use in commands. - -``self.interface.alchemy`` is an instance of :py:class:`royalnet.database.Alchemy`, which contains the -:py:class:`sqlalchemy.engine.Engine`, metadata and tables, while ``data.session`` is a -:py:class:`sqlalchemy.orm.session.Session`, and can be interacted in the same way as one. - -If you want to use :py:class:`royalnet.database.Alchemy` in your command, you should override the -``tables`` field with the :py:class:`set` of Alchemy tables you need. :: - - from royalnet.commands import Command - from royalnet.packs.common.tables import User - - class SpaghettiCommand(Command): - name = "spaghetti" - - description = "Send a spaghetti emoji in the chat." - - syntax = "{pasta}" - - tables = {User} - - ... - -Querying the database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can :py:class:`sqlalchemy.orm.query.Query` the database using the SQLAlchemy ORM. - -The SQLAlchemy tables can be found inside :py:class:`royalnet.database.Alchemy` with the same name they were created -from, if they were specified in ``tables``. :: - - query = data.session.query(User) - -Adding filters to the query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can filter the query results with the :py:meth:`sqlalchemy.orm.query.Query.filter` method. - -.. note:: Remember to always use a table column as first comparision element, as it won't work otherwise. - -:: - - query = query.filter(User.role == "Member") - - -Ordering the results of a query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can order the query results in **ascending order** with the :py:meth:`sqlalchemy.orm.query.Query.order_by` method. :: - - query = query.order_by(User.username) - -Additionally, you can append the `.desc()` method to a table column to sort in **descending order**: :: - - query = query.order_by(User.username.desc()) - -Fetching the results of a query -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can fetch the query results with the :py:meth:`sqlalchemy.orm.query.Query.all`, -:py:meth:`sqlalchemy.orm.query.Query.first`, :py:meth:`sqlalchemy.orm.query.Query.one` and -:py:meth:`sqlalchemy.orm.query.Query.one_or_none` methods. - -Remember to use :py:func:`royalnet.utils.asyncify` when fetching results, as it may take a while! - -Use :py:meth:`sqlalchemy.orm.query.Query.all` if you want a :py:class:`list` of **all results**: :: - - results: list = await asyncify(query.all) - -Use :py:meth:`sqlalchemy.orm.query.Query.first` if you want **the first result** of the list, or :py:const:`None` if -there are no results: :: - - result: typing.Union[..., None] = await asyncify(query.first) - -Use :py:meth:`sqlalchemy.orm.query.Query.one` if you expect to have **a single result**, and you want the command to -raise an error if any different number of results is returned: :: - - result: ... = await asyncify(query.one) # Raises an error if there are no results or more than a result. - -Use :py:meth:`sqlalchemy.orm.query.Query.one_or_none` if you expect to have **a single result**, or **nothing**, and -if you want the command to raise an error if the number of results is greater than one. :: - - result: typing.Union[..., None] = await asyncify(query.one_or_none) # Raises an error if there is more than a result. - -More Alchemy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can read more about :py:mod:`sqlalchemy` at their `website `_. - -Comunicating via Royalnet ------------------------------------- - -This section is not documented yet. - -Adding the command to a Pack ------------------------------------- - -This section is not documented yet. \ No newline at end of file diff --git a/docs_source/index.rst b/docs_source/index.rst index 3e909434..e2605ae8 100644 --- a/docs_source/index.rst +++ b/docs_source/index.rst @@ -4,10 +4,9 @@ royalnet Welcome to the documentation of Royalnet! .. toctree:: - :maxdepth: 3 + :maxdepth: 5 - runningroyalnet - creatingacommand + randomdiscoveries apireference diff --git a/docs_source/randomdiscoveries.rst b/docs_source/randomdiscoveries.rst new file mode 100644 index 00000000..e104e7cb --- /dev/null +++ b/docs_source/randomdiscoveries.rst @@ -0,0 +1,15 @@ +Random discoveries +================== + +Here are some things that were found out while developing the bot. + +Discord websocket undocumented error codes +------------------------------------------ + +====== ===================== + Code Reason +====== ===================== +1006 Heartbeat stopped +------ --------------------- +1006 Failed authentication +====== ===================== diff --git a/docs_source/runningroyalnet.rst b/docs_source/runningroyalnet.rst deleted file mode 100644 index 7262563b..00000000 --- a/docs_source/runningroyalnet.rst +++ /dev/null @@ -1,72 +0,0 @@ -.. currentmodule:: royalnet - -Running Royalnet -==================================== - -To run a ``royalnet`` instance, you have first to download the package from ``pip``: - -The Keyring ------------------------------------- -:: - - pip install royalnet - - -To run ``royalnet``, you'll have to setup the system keyring. - -On Windows and desktop Linux, this is already configured; -on a headless Linux instance, you'll need to `manually start and unlock the keyring daemon -`_. - -Now you have to create a new ``royalnet`` configuration. Start the configuration wizard: :: - - python -m royalnet.configurator - -You'll be prompted to enter a "secrets name": this is the name of the group of API keys that will be associated with -your bot. Enter a name that you'll be able to remember. :: - - Desired secrets name [__default__]: royalgames - -You'll then be asked for a network password. - -This password is used to connect to the rest of the :py:mod:`royalnet.network`, or, if you're hosting a local Network, -it will be the necessary password to connect to it: :: - - Network password []: cosafaunapesuunafoglia - -Then you'll be asked for a Telegram Bot API token. -You can get one from `@BotFather `_. :: - - Telegram Bot API token []: 000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - -The next prompt will ask for a Discord Bot API token. -You can get one at the `Discord Developers Portal `_. :: - - Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA - -Now the configurator will ask you for a Imgur API token. -`Register an application `_ on Imgur to be supplied one. -The token should be of type "anonymous usage without user authorization". :: - - Imgur API token []: aaaaaaaaaaaaaaa - -Next, you'll be asked for a Sentry DSN. You probably won't have one, so just ignore it and press enter. :: - - Sentry DSN []: - -Now that all tokens are configured, you're ready to launch the bot! - -Running the bots ------------------------------------- - -You can run the main ``royalnet`` process by running: :: - - python3.7 -m royalnet - -To see all available options, you can run: :: - - python3.7 -m royalnet --help - -.. note:: All royalnet options should be specified **after** the word ``royalnet``, or else they will be passed to - the Python interpreter. - diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..e6786ae5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1231 @@ +[[package]] +category = "main" +description = "Async http client/server framework (asyncio)" +name = "aiohttp" +optional = true +python-versions = ">=3.5.3" +version = "3.6.2" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<4.0" +multidict = ">=4.5,<5.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "main" +description = "Timeout context manager for asyncio programs" +name = "async-timeout" +optional = true +python-versions = ">=3.5.3" +version = "3.0.1" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.7.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.9.11" + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = true +python-versions = "*" +version = "1.13.2" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "main" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.4.1" + +[[package]] +category = "main" +description = "Colored terminal output for Python's logging module" +name = "coloredlogs" +optional = true +python-versions = "*" +version = "10.0" + +[package.dependencies] +colorama = "*" +humanfriendly = ">=4.7" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +category = "main" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "2.8" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +idna = ["idna (>=2.1)"] +pep8test = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + +[[package]] +category = "main" +description = "Date parsing library designed to parse dates from HTML pages" +name = "dateparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.7.2" + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "*" +tzlocal = "*" + +[[package]] +category = "main" +description = "A python wrapper for the Discord API" +name = "discord.py" +optional = true +python-versions = ">=3.5.3" +version = "1.3.0a2152+g15d5ba8" + +[package.dependencies] +aiohttp = ">=3.6.0,<3.7.0" +websockets = ">=6.0,<7 || >7" + +[package.extras] +docs = ["sphinx (1.8.5)", "sphinxcontrib_trio (1.1.0)", "sphinxcontrib-websupport"] +voice = ["PyNaCl (1.3.0)"] + +[package.source] +reference = "15d5ba8f1ffb887f82b7c2f7af48a523a9f8a784" +type = "git" +url = "https://github.com/Steffo99/discord.py" +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.15.2" + +[[package]] +category = "main" +description = "Python bindings for FFmpeg - with complex filtering support" +name = "ffmpeg-python" +optional = true +python-versions = "*" +version = "0.2.0" + +[package.dependencies] +future = "*" + +[package.extras] +dev = ["future (0.17.1)", "numpy (1.16.4)", "pytest-mock (1.10.4)", "pytest (4.6.1)", "Sphinx (2.1.0)", "tox (3.12.1)"] + +[[package]] +category = "main" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.18.2" + +[[package]] +category = "main" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +name = "h11" +optional = true +python-versions = "*" +version = "0.8.1" + +[[package]] +category = "main" +description = "A collection of framework independent HTTP protocol utils." +marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\"" +name = "httptools" +optional = true +python-versions = "*" +version = "0.0.13" + +[[package]] +category = "main" +description = "Human friendly output for text interfaces using Python" +name = "humanfriendly" +optional = true +python-versions = "*" +version = "4.18" + +[package.dependencies] +pyreadline = "*" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.3" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.4" +version = "7.2.0" + +[[package]] +category = "main" +description = "multidict implementation" +name = "multidict" +optional = true +python-versions = ">=3.5" +version = "4.6.1" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.2" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "main" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg2" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "2.8.4" + +[[package]] +category = "main" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg2-binary" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "2.8.4" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.19" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.4.2" + +[[package]] +category = "main" +description = "Python binding to the Networking and Cryptography (NaCl) library" +name = "pynacl" +optional = true +python-versions = "*" +version = "1.3.0" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.5" + +[[package]] +category = "main" +description = "A python implmementation of GNU readline." +marker = "sys_platform == \"win32\"" +name = "pyreadline" +optional = true +python-versions = "*" +version = "2.1" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.3.0" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "main" +description = "We have made you a wrapper you can't refuse" +name = "python-telegram-bot" +optional = true +python-versions = "*" +version = "12.2.0" + +[package.dependencies] +certifi = "*" +cryptography = "*" +future = ">=0.16.0" +tornado = ">=5.1" + +[package.extras] +json = ["ujson"] +socks = ["pysocks"] + +[[package]] +category = "main" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2019.3" + +[[package]] +category = "main" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2019.11.1" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "Python client for Sentry (https://getsentry.com)" +name = "sentry-sdk" +optional = true +python-versions = "*" +version = "0.13.3" + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.10.0" + +[package.extras] +bottle = ["bottle (>=0.12.13)"] +falcon = ["falcon (>=1.4)"] +flask = ["flask (>=0.8)", "blinker (>=1.1)"] + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.13.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "2.2.1" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3,<2.0 || >2.0" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.740)", "docutils-stubs"] + +[[package]] +category = "dev" +description = "Read the Docs theme for Sphinx" +name = "sphinx-rtd-theme" +optional = false +python-versions = "*" +version = "0.4.3" + +[package.dependencies] +sphinx = "*" + +[[package]] +category = "dev" +description = "" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "" +name = "sphinxcontrib-devhelp" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = "*" +version = "1.0.2" + +[package.extras] +test = ["pytest", "flake8", "mypy", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "" +name = "sphinxcontrib-qthelp" +optional = false +python-versions = "*" +version = "1.0.2" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "" +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = "*" +version = "1.1.3" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "main" +description = "Database Abstraction Library" +name = "sqlalchemy" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.11" + +[package.extras] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mysql = ["mysqlclient"] +oracle = ["cx-oracle"] +postgresql = ["psycopg2"] +postgresql_pg8000 = ["pg8000"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql"] + +[[package]] +category = "main" +description = "The little ASGI library that shines." +name = "starlette" +optional = true +python-versions = ">=3.6" +version = "0.12.13" + +[package.extras] +full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] + +[[package]] +category = "main" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "main" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +name = "tornado" +optional = true +python-versions = ">= 3.5" +version = "6.0.3" + +[[package]] +category = "main" +description = "tzinfo object for the local timezone" +name = "tzlocal" +optional = false +python-versions = "*" +version = "2.0.0" + +[package.dependencies] +pytz = "*" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.25.7" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "The lightning-fast ASGI server." +name = "uvicorn" +optional = true +python-versions = "*" +version = "0.10.8" + +[package.dependencies] +click = ">=7.0.0,<8.0.0" +h11 = ">=0.8.0,<0.9.0" +httptools = "0.0.13" +uvloop = ">=0.14.0" +websockets = ">=8.0.0,<9.0.0" + +[[package]] +category = "main" +description = "Fast implementation of asyncio event loop on top of libuv" +marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\"" +name = "uvloop" +optional = true +python-versions = "*" +version = "0.14.0" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "main" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +name = "websockets" +optional = true +python-versions = ">=3.6.1" +version = "8.1" + +[[package]] +category = "main" +description = "Yet another URL library" +name = "yarl" +optional = true +python-versions = ">=3.5.3" +version = "1.3.0" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +category = "main" +description = "YouTube video downloader" +name = "youtube-dl" +optional = true +python-versions = "*" +version = "2019.11.22" + +[extras] +alchemy_easy = ["sqlalchemy", "psycopg2_binary"] +alchemy_hard = ["sqlalchemy", "psycopg2"] +bard = ["ffmpeg_python", "youtube_dl"] +coloredlogs = ["coloredlogs"] +constellation = ["starlette", "uvicorn"] +discord = ["discord.py", "pynacl"] +herald = ["websockets"] +sentry = ["sentry_sdk"] +telegram = ["python_telegram_bot"] + +[metadata] +content-hash = "b159275a7b57094bc7fd801ba47883be1c6d1be395780dca0d2c018fc98009b7" + python-versions = "^3.8" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e"}, + {file = "aiohttp-3.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec"}, + {file = "aiohttp-3.6.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48"}, + {file = "aiohttp-3.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win32.whl", hash = "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17"}, + {file = "aiohttp-3.6.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a"}, + {file = "aiohttp-3.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win32.whl", hash = "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654"}, + {file = "aiohttp-3.6.2-py3-none-any.whl", hash = "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4"}, + {file = "aiohttp-3.6.2.tar.gz", hash = "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326"}, +] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +atomicwrites = [ + {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, + {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.7.0-py2.py3-none-any.whl", hash = "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab"}, + {file = "Babel-2.7.0.tar.gz", hash = "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"}, +] +certifi = [ + {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, + {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, +] +cffi = [ + {file = "cffi-1.13.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43"}, + {file = "cffi-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396"}, + {file = "cffi-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54"}, + {file = "cffi-1.13.2-cp27-cp27m-win32.whl", hash = "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159"}, + {file = "cffi-1.13.2-cp27-cp27m-win_amd64.whl", hash = "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97"}, + {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579"}, + {file = "cffi-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc"}, + {file = "cffi-1.13.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f"}, + {file = "cffi-1.13.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858"}, + {file = "cffi-1.13.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42"}, + {file = "cffi-1.13.2-cp34-cp34m-win32.whl", hash = "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b"}, + {file = "cffi-1.13.2-cp34-cp34m-win_amd64.whl", hash = "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20"}, + {file = "cffi-1.13.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3"}, + {file = "cffi-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25"}, + {file = "cffi-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5"}, + {file = "cffi-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c"}, + {file = "cffi-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b"}, + {file = "cffi-1.13.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04"}, + {file = "cffi-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652"}, + {file = "cffi-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57"}, + {file = "cffi-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e"}, + {file = "cffi-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"}, + {file = "cffi-1.13.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410"}, + {file = "cffi-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a"}, + {file = "cffi-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12"}, + {file = "cffi-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e"}, + {file = "cffi-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a"}, + {file = "cffi-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d"}, + {file = "cffi-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3"}, + {file = "cffi-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db"}, + {file = "cffi-1.13.2-cp38-cp38-win32.whl", hash = "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506"}, + {file = "cffi-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba"}, + {file = "cffi-1.13.2.tar.gz", hash = "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, + {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, +] +colorama = [ + {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, + {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, +] +coloredlogs = [ + {file = "coloredlogs-10.0-py2.py3-none-any.whl", hash = "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8"}, + {file = "coloredlogs-10.0.tar.gz", hash = "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36"}, +] +cryptography = [ + {file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"}, + {file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"}, + {file = "cryptography-2.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad"}, + {file = "cryptography-2.8-cp27-cp27m-win32.whl", hash = "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2"}, + {file = "cryptography-2.8-cp27-cp27m-win_amd64.whl", hash = "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912"}, + {file = "cryptography-2.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d"}, + {file = "cryptography-2.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42"}, + {file = "cryptography-2.8-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879"}, + {file = "cryptography-2.8-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d"}, + {file = "cryptography-2.8-cp34-abi3-manylinux2010_x86_64.whl", hash = "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9"}, + {file = "cryptography-2.8-cp34-cp34m-win32.whl", hash = "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c"}, + {file = "cryptography-2.8-cp34-cp34m-win_amd64.whl", hash = "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0"}, + {file = "cryptography-2.8-cp35-cp35m-win32.whl", hash = "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf"}, + {file = "cryptography-2.8-cp35-cp35m-win_amd64.whl", hash = "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793"}, + {file = "cryptography-2.8-cp36-cp36m-win32.whl", hash = "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595"}, + {file = "cryptography-2.8-cp36-cp36m-win_amd64.whl", hash = "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7"}, + {file = "cryptography-2.8-cp37-cp37m-win32.whl", hash = "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff"}, + {file = "cryptography-2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f"}, + {file = "cryptography-2.8-cp38-cp38-win32.whl", hash = "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e"}, + {file = "cryptography-2.8-cp38-cp38-win_amd64.whl", hash = "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13"}, + {file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"}, +] +dateparser = [ + {file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"}, + {file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"}, +] +"discord.py" = [] +docutils = [ + {file = "docutils-0.15.2-py2-none-any.whl", hash = "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827"}, + {file = "docutils-0.15.2-py3-none-any.whl", hash = "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0"}, + {file = "docutils-0.15.2.tar.gz", hash = "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"}, +] +ffmpeg-python = [ + {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, + {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +h11 = [ + {file = "h11-0.8.1-py2.py3-none-any.whl", hash = "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"}, + {file = "h11-0.8.1.tar.gz", hash = "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208"}, +] +httptools = [ + {file = "httptools-0.0.13.tar.gz", hash = "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"}, +] +humanfriendly = [ + {file = "humanfriendly-4.18-py2.py3-none-any.whl", hash = "sha256:23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d"}, + {file = "humanfriendly-4.18.tar.gz", hash = "sha256:33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +imagesize = [ + {file = "imagesize-1.1.0-py2.py3-none-any.whl", hash = "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8"}, + {file = "imagesize-1.1.0.tar.gz", hash = "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"}, +] +jinja2 = [ + {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, + {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +more-itertools = [ + {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, + {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, +] +multidict = [ + {file = "multidict-4.6.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:318aadf1cfb6741c555c7dd83d94f746dc95989f4f106b25b8a83dfb547f2756"}, + {file = "multidict-4.6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c890978e2b37dd0dc1bd952da9a5d9f245d4807bee33e3517e4119c48d66f8c"}, + {file = "multidict-4.6.1-cp35-cp35m-win32.whl", hash = "sha256:efaf1b18ea6c1f577b1371c0159edbe4749558bfe983e13aa24d0a0c01e1ad7b"}, + {file = "multidict-4.6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:07f9a6bf75ad675d53956b2c6a2d4ef2fa63132f33ecc99e9c24cf93beb0d10b"}, + {file = "multidict-4.6.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:42cdd649741a14b0602bf15985cad0dd4696a380081a3319cd1ead46fd0f0fab"}, + {file = "multidict-4.6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:205a011e636d885af6dd0029e41e3514a46e05bb2a43251a619a6e8348b96fc0"}, + {file = "multidict-4.6.1-cp36-cp36m-win32.whl", hash = "sha256:cfec9d001a83dc73580143f3c77e898cf7ad78b27bb5e64dbe9652668fcafec7"}, + {file = "multidict-4.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8d919034420378132d074bf89df148d0193e9780c9fe7c0e495e895b8af4d8a2"}, + {file = "multidict-4.6.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a37433ce8cdb35fc9e6e47e1606fa1bfd6d70440879038dca7d8dd023197eaa9"}, + {file = "multidict-4.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b605272c558e4c659dbaf0fb32a53bfede44121bcf77b356e6e906867b958b7"}, + {file = "multidict-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:891b7e142885e17a894d9d22b0349b92bb2da4769b4e675665d0331c08719be5"}, + {file = "multidict-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:250632316295f2311e1ed43e6b26a63b0216b866b45c11441886ac1543ca96e1"}, + {file = "multidict-4.6.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:2bc9c2579312c68a3552ee816311c8da76412e6f6a9cf33b15152e385a572d2a"}, + {file = "multidict-4.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0ffe4d4d28cbe9801952bfb52a8095dd9ffecebd93f84bdf973c76300de783c5"}, + {file = "multidict-4.6.1-cp38-cp38-win32.whl", hash = "sha256:87e26d8b89127c25659e962c61a4c655ec7445d19150daea0759516884ecb8b4"}, + {file = "multidict-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:c626029841ada34c030b94a00c573a0c7575fe66489cde148785b6535397d675"}, + {file = "multidict-4.6.1.tar.gz", hash = "sha256:5159c4975931a1a78bf6602bbebaa366747fce0a56cb2111f44789d2c45e379f"}, +] +packaging = [ + {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, + {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +psycopg2 = [ + {file = "psycopg2-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b"}, + {file = "psycopg2-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0"}, + {file = "psycopg2-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38"}, + {file = "psycopg2-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6"}, + {file = "psycopg2-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a"}, + {file = "psycopg2-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4"}, + {file = "psycopg2-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151"}, + {file = "psycopg2-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b"}, + {file = "psycopg2-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c"}, + {file = "psycopg2-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d"}, + {file = "psycopg2-2.8.4-cp38-cp38-win32.whl", hash = "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677"}, + {file = "psycopg2-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f"}, + {file = "psycopg2-2.8.4.tar.gz", hash = "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6"}, +] +psycopg2-binary = [ + {file = "psycopg2-binary-2.8.4.tar.gz", hash = "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9"}, + {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b"}, + {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309"}, + {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e"}, + {file = "psycopg2_binary-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29"}, + {file = "psycopg2_binary-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49"}, + {file = "psycopg2_binary-2.8.4-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881"}, + {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e"}, + {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"}, + {file = "psycopg2_binary-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03"}, + {file = "psycopg2_binary-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e"}, + {file = "psycopg2_binary-2.8.4-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3"}, + {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d"}, + {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd"}, + {file = "psycopg2_binary-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f"}, + {file = "psycopg2_binary-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7"}, + {file = "psycopg2_binary-2.8.4-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b"}, + {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964"}, + {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70"}, + {file = "psycopg2_binary-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03"}, + {file = "psycopg2_binary-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8"}, + {file = "psycopg2_binary-2.8.4-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b"}, + {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039"}, + {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103"}, + {file = "psycopg2_binary-2.8.4-cp38-cp38-win32.whl", hash = "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1"}, + {file = "psycopg2_binary-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f"}, +] +py = [ + {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, + {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, +] +pycparser = [ + {file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"}, +] +pygments = [ + {file = "Pygments-2.4.2-py2.py3-none-any.whl", hash = "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127"}, + {file = "Pygments-2.4.2.tar.gz", hash = "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"}, +] +pynacl = [ + {file = "PyNaCl-1.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621"}, + {file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39"}, + {file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255"}, + {file = "PyNaCl-1.3.0-cp27-cp27m-win32.whl", hash = "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f"}, + {file = "PyNaCl-1.3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"}, + {file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1"}, + {file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e"}, + {file = "PyNaCl-1.3.0-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1"}, + {file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_i686.whl", hash = "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786"}, + {file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415"}, + {file = "PyNaCl-1.3.0-cp34-cp34m-win32.whl", hash = "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b"}, + {file = "PyNaCl-1.3.0-cp34-cp34m-win_amd64.whl", hash = "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae"}, + {file = "PyNaCl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310"}, + {file = "PyNaCl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a"}, + {file = "PyNaCl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20"}, + {file = "PyNaCl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b"}, + {file = "PyNaCl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56"}, + {file = "PyNaCl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715"}, + {file = "PyNaCl-1.3.0-cp38-cp38-win32.whl", hash = "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5"}, + {file = "PyNaCl-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92"}, + {file = "PyNaCl-1.3.0.tar.gz", hash = "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c"}, +] +pyparsing = [ + {file = "pyparsing-2.4.5-py2.py3-none-any.whl", hash = "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f"}, + {file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"}, +] +pyreadline = [ + {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, + {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, + {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, +] +pytest = [ + {file = "pytest-5.3.0-py3-none-any.whl", hash = "sha256:f6a567e20c04259d41adce9a360bd8991e6aa29dd9695c5e6bd25a9779272673"}, + {file = "pytest-5.3.0.tar.gz", hash = "sha256:1897d74f60a5d8be02e06d708b41bf2445da2ee777066bd68edf14474fc201eb"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +python-telegram-bot = [ + {file = "python-telegram-bot-12.2.0.tar.gz", hash = "sha256:346d42771c2b23384c59f5f41e05bd7e801a0ce118d8dcb95209bb73d5f694c5"}, + {file = "python_telegram_bot-12.2.0-py2.py3-none-any.whl", hash = "sha256:3beee89cba3bc3217566c96199f04776dd25f541ac8992da27fd247b2d208a14"}, +] +pytz = [ + {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, + {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, +] +regex = [ + {file = "regex-2019.11.1-cp27-none-win32.whl", hash = "sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1"}, + {file = "regex-2019.11.1-cp27-none-win_amd64.whl", hash = "sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96"}, + {file = "regex-2019.11.1-cp35-none-win32.whl", hash = "sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7"}, + {file = "regex-2019.11.1-cp35-none-win_amd64.whl", hash = "sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6"}, + {file = "regex-2019.11.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59"}, + {file = "regex-2019.11.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143"}, + {file = "regex-2019.11.1-cp36-none-win32.whl", hash = "sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910"}, + {file = "regex-2019.11.1-cp36-none-win_amd64.whl", hash = "sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a"}, + {file = "regex-2019.11.1-cp37-none-win32.whl", hash = "sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2"}, + {file = "regex-2019.11.1-cp37-none-win_amd64.whl", hash = "sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7"}, + {file = "regex-2019.11.1-cp38-none-win32.whl", hash = "sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"}, + {file = "regex-2019.11.1-cp38-none-win_amd64.whl", hash = "sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66"}, + {file = "regex-2019.11.1.tar.gz", hash = "sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +sentry-sdk = [ + {file = "sentry-sdk-0.13.3.tar.gz", hash = "sha256:e3302e8df82e68599eeeef564f08d15aa62efc1cb013d8e1cccc5bf526d375a4"}, + {file = "sentry_sdk-0.13.3-py2.py3-none-any.whl", hash = "sha256:e795f1744066493f9e1eb3d17e0ee19a042a45789b9edd9f553b8b61bc8d399e"}, +] +six = [ + {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, + {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-2.2.1-py3-none-any.whl", hash = "sha256:52286a0b9d7caa31efee301ec4300dbdab23c3b05da1c9024b4e84896fb73d79"}, + {file = "Sphinx-2.2.1.tar.gz", hash = "sha256:31088dfb95359384b1005619827eaee3056243798c62724fd3fa4b84ee4d71bd"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.4.3-py2.py3-none-any.whl", hash = "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4"}, + {file = "sphinx_rtd_theme-0.4.3.tar.gz", hash = "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"}, + {file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"}, + {file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.2.tar.gz", hash = "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422"}, + {file = "sphinxcontrib_htmlhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"}, + {file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.3.tar.gz", hash = "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227"}, + {file = "sphinxcontrib_serializinghtml-1.1.3-py2.py3-none-any.whl", hash = "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.3.11.tar.gz", hash = "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"}, +] +starlette = [ + {file = "starlette-0.12.13.tar.gz", hash = "sha256:9597bc28e3c4659107c1c4a45ec32dc45e947d78fe56230222be673b2c36454a"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +tornado = [ + {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, + {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, + {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, + {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, + {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, + {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, + {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, +] +tzlocal = [ + {file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"}, + {file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"}, +] +urllib3 = [ + {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, + {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, +] +uvicorn = [ + {file = "uvicorn-0.10.8.tar.gz", hash = "sha256:f4c34642618449f55e2bab8c6b22ff7615b520d2e7e23275be2ca894254327a3"}, +] +uvloop = [ + {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, + {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, + {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, + {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, + {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, + {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, + {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, + {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, + {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, +] +wcwidth = [ + {file = "wcwidth-0.1.7-py2.py3-none-any.whl", hash = "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"}, + {file = "wcwidth-0.1.7.tar.gz", hash = "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"}, +] +websockets = [ + {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, + {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, + {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, + {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, + {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, + {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, + {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, + {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, + {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, + {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, + {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, + {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, + {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, + {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, + {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, + {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, + {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, + {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, + {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, + {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, + {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, + {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, +] +yarl = [ + {file = "yarl-1.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320"}, + {file = "yarl-1.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb"}, + {file = "yarl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829"}, + {file = "yarl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310"}, + {file = "yarl-1.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f"}, + {file = "yarl-1.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842"}, + {file = "yarl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8"}, + {file = "yarl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4"}, + {file = "yarl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"}, + {file = "yarl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0"}, + {file = "yarl-1.3.0.tar.gz", hash = "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9"}, +] +youtube-dl = [ + {file = "youtube_dl-2019.11.22-py2.py3-none-any.whl", hash = "sha256:bd785113687f201415389156664b9ebd81698fb6eb44c6d9fd35898619e27bf7"}, + {file = "youtube_dl-2019.11.22.tar.gz", hash = "sha256:0575efd332cb9817f5a1fffd2a1e569e5a7d3642e7c24c7a5c47cbf70f301f25"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..7d039f5c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +# Remember to run `poetry update` after you edit this file! + +[tool.poetry] + name = "royalnet" + version = "5.1a1" + description = "A multipurpose bot and web framework" + authors = ["Stefano Pigozzi "] + license = "AGPL-3.0+" + readme = "README.md" + homepage = "https://github.com/Steffo99/royalnet" + documentation = "https://gh.steffo.eu/royalnet/" + classifiers = [ + "Development Status :: 3 - Alpha", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)" + ] + +# Library dependencies +[tool.poetry.dependencies] + python = "^3.8" + dateparser = "^0.7.2" + toml = "^0.10.0" + + # telegram + python_telegram_bot = {version="^12.2.0", optional=true} + + # discord + "discord.py" = {git="https://github.com/Steffo99/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods + pynacl = {version="^1.3.0", optional=true} # This requires libffi-dev and python3.*-dev to be installed on Linux systems + + # bard + ffmpeg_python = {version="~0.2.0", optional=true} + youtube_dl = {version="*", optional=true} + + # alchemy + sqlalchemy = {version="^1.3.10", optional=true} + psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source + psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended + + # constellation + starlette = {version="^0.12.13", optional=true} + uvicorn = {version="^0.10.7", optional=true} + + # sentry + sentry_sdk = {version="~0.13.2", optional=true} + + # herald + websockets = {version="^8.1", optional=true} + + # logging + coloredlogs = {version="^10.0", optional=true} + +# Development dependencies +[tool.poetry.dev-dependencies] + pytest = "^5.2.1" + sphinx = "^2.2.1" + sphinx_rtd_theme = "^0.4.3" + + +# Optional dependencies +[tool.poetry.extras] + telegram = ["python_telegram_bot"] + discord = ["discord.py", "pynacl"] + alchemy_easy = ["sqlalchemy", "psycopg2_binary"] + alchemy_hard = ["sqlalchemy", "psycopg2"] + bard = ["ffmpeg_python", "youtube_dl"] + constellation = ["starlette", "uvicorn"] + sentry = ["sentry_sdk"] + herald = ["websockets"] + coloredlogs = ["coloredlogs"] + +[build-system] + requires = ["poetry>=0.12"] + build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 822dedbe..00000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -dateparser -youtube_dl -ffmpeg_python -urllib3 -sqlalchemy -starlette -keyring -click -royalherald diff --git a/royalnet/__init__.py b/royalnet/__init__.py index c43072c8..3a1111c2 100644 --- a/royalnet/__init__.py +++ b/royalnet/__init__.py @@ -1,3 +1,14 @@ -from . import audio, bots, commands, packs, database, utils, error, web, version +from . import alchemy, bard, commands, constellation, herald, backpack, serf, utils, version -__all__ = ["audio", "bots", "commands", "database", "utils", "error", "web", "version"] +__version__ = version.semantic + +__all__ = [ + "alchemy", + "bard", + "commands", + "constellation", + "herald", + "serf", + "utils", + "backpack", +] diff --git a/royalnet/__main__.py b/royalnet/__main__.py index a168ebf9..8c429fc0 100644 --- a/royalnet/__main__.py +++ b/royalnet/__main__.py @@ -1,193 +1,126 @@ import click -import typing -import importlib -import royalnet as r -import royalherald as rh import multiprocessing -import keyring +import royalnet.constellation as rc +import royalnet.serf as rs +import royalnet.utils as ru +import royalnet.herald as rh +import toml import logging +try: + import coloredlogs +except ImportError: + coloredlogs = None + + +log = logging.getLogger(__name__) + @click.command() -@click.option("--telegram/--no-telegram", default=None, - help="Enable/disable the Telegram bot.") -@click.option("--discord/--no-discord", default=None, - help="Enable/disable the Discord bot.") -@click.option("--webserver/--no-webserver", default=None, - help="Enable/disable the Web server.") -@click.option("--webserver-port", default=8001, - help="The port on which the web server will listen on.") -@click.option("-d", "--database", type=str, default=None, - help="The PostgreSQL database path.") -@click.option("-p", "--packs", type=str, multiple=True, default=[], - help="The names of the Packs that should be used.") -@click.option("-n", "--network-address", type=str, default=None, - help="The Network server URL to connect to.") -@click.option("-l", "--local-network-server", is_flag=True, default=False, - help="Locally run a Network server and bind it to port 44444. Overrides -n.") -@click.option("--local-network-server-port", type=int, default=44444, - help="The port on which the local network will be ran.") -@click.option("-s", "--secrets-name", type=str, default="__default__", - help="The name in the keyring that the secrets are stored with.") -@click.option("-v", "--verbose", is_flag=True, default=False, - help="Print all possible debug information.") -def run(telegram: typing.Optional[bool], - discord: typing.Optional[bool], - webserver: typing.Optional[bool], - webserver_port: typing.Optional[int], - database: typing.Optional[str], - packs: typing.Tuple[str], - network_address: typing.Optional[str], - local_network_server: bool, - local_network_server_port: int, - secrets_name: str, - verbose: bool): - # Setup logging - if verbose: - core_logger = logging.root - core_logger.setLevel(logging.DEBUG) - stream_handler = logging.StreamHandler() - stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") - core_logger.addHandler(stream_handler) - core_logger.debug("Logging setup complete.") +@click.option("-c", "--config-filename", default="./config.toml", type=str, + help="The filename of the Royalnet configuration file.") +def run(config_filename: str): + # Read the configuration file + with open(config_filename, "r") as t: + config: dict = toml.load(t) - # Get the network password - network_password = keyring.get_password(f"Royalnet/{secrets_name}", "network") + ru.init_logging(config["Logging"]) - # Get the sentry dsn - sentry_dsn = keyring.get_password(f"Royalnet/{secrets_name}", "sentry") - - # Enable / Disable interfaces - interfaces = { - "telegram": telegram, - "discord": discord, - "webserver": webserver - } - # If any interface is True, then the undefined ones should be False - if any(interfaces[name] is True for name in interfaces): - for name in interfaces: - if interfaces[name] is None: - interfaces[name] = False - # Likewise, if any interface is False, then the undefined ones should be True - elif any(interfaces[name] is False for name in interfaces): - for name in interfaces: - if interfaces[name] is None: - interfaces[name] = True - # Otherwise, if no interfaces are specified, all should be enabled + if config["Sentry"] is None or not config["Sentry"]["enabled"]: + log.info("Sentry: disabled") else: - assert all(interfaces[name] is None for name in interfaces) - for name in interfaces: - interfaces[name] = True - - server_process: typing.Optional[multiprocessing.Process] = None - # Start the network server - if local_network_server: - server_process = multiprocessing.Process(name="Network Server", - target=rh.Server("0.0.0.0", local_network_server_port, network_password).run_blocking, - daemon=True) - server_process.start() - network_address = f"ws://127.0.0.1:{local_network_server_port}/" - - # Create a Royalnet configuration - network_config: typing.Optional[rh.Config] = None - if network_address is not None: - network_config = rh.Config(network_address, network_password) - - # Create a Alchemy configuration - telegram_db_config: typing.Optional[r.database.DatabaseConfig] = None - discord_db_config: typing.Optional[r.database.DatabaseConfig] = None - if database is not None: - telegram_db_config = r.database.DatabaseConfig(database, - r.packs.common.tables.User, - r.packs.common.tables.Telegram, - "tg_id") - discord_db_config = r.database.DatabaseConfig(database, - r.packs.common.tables.User, - r.packs.common.tables.Discord, - "discord_id") - - # Import command and star packs - packs: typing.List[str] = list(packs) - packs.append("royalnet.packs.common") # common pack is always imported - enabled_commands = [] - enabled_page_stars = [] - enabled_exception_stars = [] - for pack in packs: - imported = importlib.import_module(pack) try: - imported_commands = imported.available_commands - except AttributeError: - raise click.ClickException(f"{pack} isn't a Royalnet Pack as it is missing available_commands.") - try: - imported_page_stars = imported.available_page_stars - except AttributeError: - raise click.ClickException(f"{pack} isn't a Royalnet Pack as it is missing available_page_stars.") - try: - imported_exception_stars = imported.available_exception_stars - except AttributeError: - raise click.ClickException(f"{pack} isn't a Royalnet Pack as it is missing available_exception_stars.") - enabled_commands = [*enabled_commands, *imported_commands] - enabled_page_stars = [*enabled_page_stars, *imported_page_stars] - enabled_exception_stars = [*enabled_exception_stars, *imported_exception_stars] + ru.init_sentry(config["Sentry"]) + except ImportError: + log.info("Sentry: not installed") - telegram_process: typing.Optional[multiprocessing.Process] = None - if interfaces["telegram"]: - click.echo("\n@BotFather Commands String") - for command in enabled_commands: - click.echo(f"{command.name} - {command.description}") - click.echo("") - telegram_bot = r.bots.TelegramBot(network_config=network_config, - database_config=telegram_db_config, - sentry_dsn=sentry_dsn, - commands=enabled_commands, - secrets_name=secrets_name) - telegram_process = multiprocessing.Process(name="Telegram Interface", - target=telegram_bot.run_blocking, - args=(verbose,), - daemon=True) + # Herald Server + herald_cfg = None + herald_process = None + if config["Herald"]["Local"]["enabled"]: + # Create a Herald server + herald_server = rh.Server(rh.Config.from_config(name="", **config["Herald"]["Local"])) + # Run the Herald server on a new process + herald_process = multiprocessing.Process(name="Herald.Local", + target=herald_server.run_blocking, + daemon=True, + kwargs={ + "logging_cfg": config["Logging"] + }) + herald_process.start() + herald_cfg = config["Herald"]["Local"] + log.info("Herald: Enabled (Local)") + elif config["Herald"]["Remote"]["enabled"]: + log.info("Herald: Enabled (Remote)") + herald_cfg = config["Herald"]["Remote"] + else: + log.info("Herald: Disabled") + + # Serfs + telegram_process = None + if config["Serfs"]["Telegram"]["enabled"]: + telegram_process = multiprocessing.Process(name="Serf.Telegram", + target=rs.telegram.TelegramSerf.run_process, + daemon=True, + kwargs={ + "alchemy_cfg": config["Alchemy"], + "herald_cfg": herald_cfg, + "packs_cfg": config["Packs"], + "sentry_cfg": config["Sentry"], + "logging_cfg": config["Logging"], + "serf_cfg": config["Serfs"]["Telegram"], + }) telegram_process.start() + log.info("Serf.Telegram: Started") + else: + log.info("Serf.Telegram: Disabled") - discord_process: typing.Optional[multiprocessing.Process] = None - if interfaces["discord"]: - discord_bot = r.bots.DiscordBot(network_config=network_config, - database_config=discord_db_config, - sentry_dsn=sentry_dsn, - commands=enabled_commands, - secrets_name=secrets_name) - discord_process = multiprocessing.Process(name="Discord Interface", - target=discord_bot.run_blocking, - args=(verbose,), - daemon=True) + discord_process = None + if config["Serfs"]["Discord"]["enabled"]: + discord_process = multiprocessing.Process(name="Serf.Discord", + target=rs.discord.DiscordSerf.run_process, + daemon=True, + kwargs={ + "alchemy_cfg": config["Alchemy"], + "herald_cfg": herald_cfg, + "packs_cfg": config["Packs"], + "sentry_cfg": config["Sentry"], + "logging_cfg": config["Logging"], + "serf_cfg": config["Serfs"]["Discord"], + }) discord_process.start() + log.info("Serf.Discord: Started") + else: + log.info("Serf.Discord: Disabled") - webserver_process: typing.Optional[multiprocessing.Process] = None - if interfaces["webserver"]: - # Common tables are always included - constellation_tables = set(r.packs.common.available_tables) - # Find the required tables - for star in [*enabled_page_stars, *enabled_exception_stars]: - constellation_tables = constellation_tables.union(star.tables) - # Create the Constellation - constellation = r.web.Constellation(page_stars=enabled_page_stars, - exc_stars=enabled_exception_stars, - secrets_name=secrets_name, - database_uri=database, - tables=constellation_tables) - webserver_process = multiprocessing.Process(name="Constellation Webserver", - target=constellation.run_blocking, - args=("0.0.0.0", webserver_port, verbose,), - daemon=True) - webserver_process.start() + # Constellation + constellation_process = None + if config["Constellation"]["enabled"]: + constellation_process = multiprocessing.Process(name="Constellation", + target=rc.Constellation.run_process, + daemon=True, + kwargs={ + "alchemy_cfg": config["Alchemy"], + "herald_cfg": herald_cfg, + "packs_cfg": config["Packs"], + "sentry_cfg": config["Sentry"], + "logging_cfg": config["Logging"], + "constellation_cfg": config["Constellation"], + }) + constellation_process.start() + log.info("Constellation: Started") + else: + log.info("Constellation: Disabled") - click.echo("Royalnet processes have been started. You can force-quit by pressing Ctrl+C.") - if server_process is not None: - server_process.join() + log.info("All processes started!") + if constellation_process is not None: + constellation_process.join() if telegram_process is not None: telegram_process.join() if discord_process is not None: discord_process.join() - if webserver_process is not None: - webserver_process.join() + if herald_process is not None: + herald_process.join() if __name__ == "__main__": diff --git a/royalnet/__pycache__/__init__.cpython-37.pyc b/royalnet/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 00000000..664e5ff0 Binary files /dev/null and b/royalnet/__pycache__/__init__.cpython-37.pyc differ diff --git a/royalnet/__pycache__/configurator.cpython-37.pyc b/royalnet/__pycache__/configurator.cpython-37.pyc new file mode 100644 index 00000000..154e820c Binary files /dev/null and b/royalnet/__pycache__/configurator.cpython-37.pyc differ diff --git a/royalnet/__pycache__/error.cpython-37.pyc b/royalnet/__pycache__/error.cpython-37.pyc new file mode 100644 index 00000000..e99f10e7 Binary files /dev/null and b/royalnet/__pycache__/error.cpython-37.pyc differ diff --git a/royalnet/__pycache__/version.cpython-37.pyc b/royalnet/__pycache__/version.cpython-37.pyc new file mode 100644 index 00000000..42dcc8f7 Binary files /dev/null and b/royalnet/__pycache__/version.cpython-37.pyc differ diff --git a/royalnet/alchemy/__init__.py b/royalnet/alchemy/__init__.py new file mode 100644 index 00000000..c0f8070f --- /dev/null +++ b/royalnet/alchemy/__init__.py @@ -0,0 +1,12 @@ +"""Relational database classes and methods.""" + +from .alchemy import Alchemy +from .table_dfs import table_dfs +from .errors import * + +__all__ = [ + "Alchemy", + "table_dfs", + "AlchemyException", + "TableNotFoundError" +] diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py new file mode 100644 index 00000000..65af1414 --- /dev/null +++ b/royalnet/alchemy/alchemy.py @@ -0,0 +1,121 @@ +from typing import Set, Dict, Union +from contextlib import contextmanager, asynccontextmanager +from royalnet.utils import asyncify +from royalnet.alchemy.errors import TableNotFoundError + +try: + from sqlalchemy import create_engine + from sqlalchemy.engine import Engine + from sqlalchemy.schema import Table + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.ext.declarative.api import DeclarativeMeta + from sqlalchemy.orm import sessionmaker +except ImportError: + create_engine = None + Engine = None + Table = None + declarative_base = None + DeclarativeMeta = None + sessionmaker = None + + +class Alchemy: + """A wrapper around :mod:`sqlalchemy.orm` that allows the instantiation of multiple engines at once while maintaining + a single declarative class for all of them.""" + + def __init__(self, database_uri: str, tables: Set): + """Create a new :class:`.Alchemy` object. + + Args: + database_uri: The `database URI `_ . + tables: The :class:`set` of tables to be created and used in the selected database. + Check the tables submodule for more details. + """ + if create_engine is None: + raise ImportError("'alchemy' extra is not installed") + + if database_uri.startswith("sqlite"): + raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded" + " applications") + self._engine: Engine = create_engine(database_uri) + self._Base: DeclarativeMeta = declarative_base(bind=self._engine) + self.Session: sessionmaker = sessionmaker(bind=self._engine) + self._tables: Dict[str, Table] = {} + for table in tables: + name = table.__name__ + assert self._tables.get(name) is None + assert isinstance(name, str) + # noinspection PyTypeChecker + bound_table: Table = type(name, (self._Base, table), {}) + self._tables[name] = bound_table + self._Base.metadata.create_all() + + def get(self, table: Union[str, type]) -> Table: + """Get the table with a specified name or class. + + Args: + table: The table name or table class you want to get. + + Raises: + TableNotFoundError: if the requested table was not found.""" + if isinstance(table, str): + result = self._tables.get(table) + if result is None: + raise TableNotFoundError(f"Table '{table}' isn't present in this Alchemy instance") + return result + elif isinstance(table, type): + name = table.__name__ + result = self._tables.get(name) + if result is None: + raise TableNotFoundError(f"Table '{table}' isn't present in this Alchemy instance") + return result + else: + raise TypeError(f"Can't get tables with objects of type '{table.__class__.__qualname__}'") + + @contextmanager + def session_cm(self): + """Create a Session as a context manager (that can be used in ``with`` statements). + + The Session will be closed safely when the context manager exits (even in case of error). + + Example: + You can use the context manager like this: :: + + with alchemy.session_cm() as session: + # Do some stuff + ... + # Commit the session + session.commit() + + """ + session = self.Session() + try: + yield session + except Exception: + session.rollback() + raise + finally: + session.close() + + @asynccontextmanager + async def session_acm(self): + """Create a Session as a async context manager (that can be used in ``async with`` statements). + + The Session will be closed safely when the context manager exits (even in case of error). + + Example: + You can use the async context manager like this: :: + + async with alchemy.session_acm() as session: + # Do some stuff + ... + # Commit the session + await asyncify(session.commit)""" + session = await asyncify(self.Session) + try: + yield session + except Exception: + session.rollback() + raise + finally: + session.close() diff --git a/royalnet/alchemy/errors.py b/royalnet/alchemy/errors.py new file mode 100644 index 00000000..12e5a858 --- /dev/null +++ b/royalnet/alchemy/errors.py @@ -0,0 +1,6 @@ +class AlchemyException(Exception): + """Base class for Alchemy exceptions.""" + + +class TableNotFoundError(AlchemyException): + """The requested table was not found.""" diff --git a/royalnet/alchemy/table_dfs.py b/royalnet/alchemy/table_dfs.py new file mode 100644 index 00000000..f9dbbdfa --- /dev/null +++ b/royalnet/alchemy/table_dfs.py @@ -0,0 +1,32 @@ +try: + from sqlalchemy.inspection import inspect + from sqlalchemy.schema import Table +except ImportError: + inspect = None + Table = None + + +def table_dfs(starting_table: Table, ending_table: Table) -> tuple: + """Depth-first-search for the path from the starting table to the ending table. + + Returns: + A :class:`tuple` containing the path, starting from the starting table and ending at the ending table.""" + if inspect is None: + raise ImportError("'alchemy' extra is not installed") + + inspected = set() + + def search(_mapper, chain): + inspected.add(_mapper) + if _mapper.class_ == ending_table: + return chain + relationships = _mapper.relationships + for _relationship in set(relationships): + if _relationship.mapper in inspected: + continue + result = search(_relationship.mapper, chain + (_relationship,)) + if len(result) != 0: + return result + return () + + return search(inspect(starting_table), tuple()) diff --git a/royalnet/audio/__init__.py b/royalnet/audio/__init__.py deleted file mode 100644 index 3cbe9192..00000000 --- a/royalnet/audio/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Video and audio downloading related classes, mainly used for Discord voice bots.""" - -from . import playmodes -from .ytdlinfo import YtdlInfo -from .ytdlfile import YtdlFile -from .fileaudiosource import FileAudioSource -from .ytdldiscord import YtdlDiscord -from .ytdlmp3 import YtdlMp3 - -__all__ = ["playmodes", "YtdlInfo", "YtdlFile", "FileAudioSource", "YtdlDiscord", "YtdlMp3"] diff --git a/royalnet/audio/errors.py b/royalnet/audio/errors.py deleted file mode 100644 index e6fddb89..00000000 --- a/royalnet/audio/errors.py +++ /dev/null @@ -1,18 +0,0 @@ -class YtdlError(Exception): - pass - - -class NotFoundError(YtdlError): - pass - - -class MultipleFilesError(YtdlError): - pass - - -class MissingInfoError(YtdlError): - pass - - -class AlreadyDownloadedError(YtdlError): - pass diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py deleted file mode 100644 index 10c08929..00000000 --- a/royalnet/audio/playmodes.py +++ /dev/null @@ -1,213 +0,0 @@ -import math -import random -import typing -from collections import namedtuple -from .ytdldiscord import YtdlDiscord -from .fileaudiosource import FileAudioSource - - -class PlayMode: - """The base class for a PlayMode, such as :py:class:`royalnet.audio.Playlist`. Inherit from this class if you want to create a custom PlayMode.""" - - def __init__(self): - """Create a new PlayMode and initialize the generator inside.""" - self.now_playing: typing.Optional[YtdlDiscord] = None - self.generator: typing.AsyncGenerator = self._generate_generator() - - async def next(self) -> typing.Optional[FileAudioSource]: - """Get the next :py:class:`royalnet.audio.FileAudioSource` from the list and advance it. - - Returns: - The next :py:class:`royalnet.audio.FileAudioSource`.""" - return await self.generator.__anext__() - - def videos_left(self) -> typing.Union[int, float]: - """Return the number of videos left in the PlayMode. - - Returns: - Usually a :py:class:`int`, but may return also :py:obj:`math.inf` if the PlayMode is infinite.""" - raise NotImplementedError() - - async def _generate_generator(self): - """Factory function for an async generator that changes the ``now_playing`` property either to a :py:class:`royalnet.audio.FileAudioSource` or to ``None``, then yields the value it changed it to. - - Yields: - The :py:class:`royalnet.audio.FileAudioSource` to be played next.""" - raise NotImplementedError() - # This is needed to make the coroutine an async generator - # noinspection PyUnreachableCode - yield NotImplemented - - def add(self, item: YtdlDiscord) -> None: - """Add a new :py:class:`royalnet.audio.YtdlDiscord` to the PlayMode. - - Args: - item: The item to add to the PlayMode.""" - raise NotImplementedError() - - def delete(self) -> None: - """Delete all :py:class:`royalnet.audio.YtdlDiscord` contained inside this PlayMode.""" - raise NotImplementedError() - - def queue_preview(self) -> typing.List[YtdlDiscord]: - """Display all the videos in the PlayMode as a list, if possible. - - To be used with ``queue`` packs, for example. - - Raises: - NotImplementedError: If a preview can't be generated. - - Returns: - A list of videos contained in the queue.""" - raise NotImplementedError() - - -class Playlist(PlayMode): - """A video list. :py:class:`royalnet.audio.YtdlDiscord` played are removed from the list.""" - - def __init__(self, starting_list: typing.List[YtdlDiscord] = None): - """Create a new Playlist. - - Args: - starting_list: A list of items with which the Playlist will be created.""" - super().__init__() - if starting_list is None: - starting_list = [] - self.list: typing.List[YtdlDiscord] = starting_list - - def videos_left(self) -> typing.Union[int, float]: - return len(self.list) - - async def _generate_generator(self): - while True: - try: - next_video = self.list.pop(0) - except IndexError: - self.now_playing = None - yield None - else: - self.now_playing = next_video - yield next_video.spawn_audiosource() - if self.now_playing is not None: - self.now_playing.delete() - - def add(self, item) -> None: - self.list.append(item) - - def delete(self) -> None: - if self.now_playing is not None: - self.now_playing.delete() - while self.list: - self.list.pop(0).delete() - - def queue_preview(self) -> typing.List[YtdlDiscord]: - return self.list - - -class Pool(PlayMode): - """A random pool. :py:class:`royalnet.audio.YtdlDiscord` are selected in random order and are not repeated until every song has been played at least once.""" - - def __init__(self, starting_pool: typing.List[YtdlDiscord] = None): - """Create a new Pool. - - Args: - starting_pool: A list of items the Pool will be created from.""" - super().__init__() - if starting_pool is None: - starting_pool = [] - self.pool: typing.List[YtdlDiscord] = starting_pool - self._pool_copy: typing.List[YtdlDiscord] = [] - - def videos_left(self) -> typing.Union[int, float]: - return math.inf - - async def _generate_generator(self): - while True: - if not self.pool: - self.now_playing = None - yield None - continue - self._pool_copy = self.pool.copy() - random.shuffle(self._pool_copy) - while self._pool_copy: - next_video = self._pool_copy.pop(0) - self.now_playing = next_video - yield next_video.spawn_audiosource() - - def add(self, item) -> None: - self.pool.append(item) - self._pool_copy.append(item) - random.shuffle(self._pool_copy) - - def delete(self) -> None: - for item in self.pool: - item.delete() - self.pool = None - self._pool_copy = None - - def queue_preview(self) -> typing.List[YtdlDiscord]: - preview_pool = self.pool.copy() - random.shuffle(preview_pool) - return preview_pool - - -class Layers(PlayMode): - """A playmode for playing a single song with multiple layers.""" - - Layer = namedtuple("Layer", ["dfile", "source"]) - - def __init__(self, starting_layers: typing.List[YtdlDiscord] = None): - super().__init__() - if starting_layers is None: - starting_layers = [] - self.layers = [] - for item in starting_layers: - self.add(item) - - def videos_left(self) -> typing.Union[int, float]: - return 1 if len(self.layers) > 0 else 0 - - async def _generate_generator(self): - current_layer = None - current_source = None - while True: - if len(self.layers) == 0: - yield None - continue - if self.now_playing is None: - self.now_playing = self.layers[0].dfile - current_source = self.layers[0].source - current_layer = 0 - yield current_source - continue - if current_source.file.closed: - self.now_playing = None - self.layers = [] - current_layer = None - current_source = None - yield None - continue - current_layer += 1 - current_position = current_source.file.tell() - if current_layer >= len(self.layers): - self.now_playing = self.layers[0].dfile - current_source = self.layers[0].source - current_source.file.seek(current_position) - current_layer = 0 - yield current_source - continue - self.now_playing = self.layers[current_layer].dfile - current_source = self.layers[current_layer].source - current_source.file.seek(current_position) - yield current_source - - def add(self, item) -> None: - self.layers.append(self.Layer(dfile=item, source=item.spawn_audiosource())) - - def delete(self) -> None: - for item in self.layers: - item.dfile.delete() - self.layers = None - - def queue_preview(self) -> typing.List[YtdlDiscord]: - return [layer.dfile for layer in self.layers] diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py deleted file mode 100644 index f178b07f..00000000 --- a/royalnet/audio/ytdldiscord.py +++ /dev/null @@ -1,72 +0,0 @@ -import typing -import re -import ffmpeg -import os -from .ytdlinfo import YtdlInfo -from .ytdlfile import YtdlFile -from .fileaudiosource import FileAudioSource - - -class YtdlDiscord: - def __init__(self, ytdl_file: YtdlFile): - self.ytdl_file: YtdlFile = ytdl_file - self.pcm_filename: typing.Optional[str] = None - self._fas_spawned: typing.List[FileAudioSource] = [] - - def __repr__(self): - return f"<{self.__class__.__name__} {self.info.title or 'Unknown Title'} ({'ready' if self.pcm_available() else 'not ready'}," \ - f" {len(self._fas_spawned)} audiosources spawned)>" - - def pcm_available(self): - return self.pcm_filename is not None and os.path.exists(self.pcm_filename) - - def convert_to_pcm(self) -> None: - if not self.ytdl_file.is_downloaded(): - raise FileNotFoundError("File hasn't been downloaded yet") - destination_filename = re.sub(r"\.[^.]+$", ".pcm", self.ytdl_file.filename) - ( - ffmpeg.input(self.ytdl_file.filename) - .output(destination_filename, format="s16le", ac=2, ar="48000") - .overwrite_output() - .run(quiet=not __debug__) - ) - self.pcm_filename = destination_filename - - def ready_up(self): - if not self.ytdl_file.has_info(): - self.ytdl_file.update_info() - if not self.ytdl_file.is_downloaded(): - self.ytdl_file.download_file() - if not self.pcm_available(): - self.convert_to_pcm() - - def spawn_audiosource(self) -> FileAudioSource: - if not self.pcm_available(): - raise FileNotFoundError("File hasn't been converted to PCM yet") - stream = open(self.pcm_filename, "rb") - source = FileAudioSource(stream) - # FIXME: it's a intentional memory leak - self._fas_spawned.append(source) - return source - - def delete(self) -> None: - if self.pcm_available(): - for source in self._fas_spawned: - if not source.file.closed: - source.file.close() - os.remove(self.pcm_filename) - self.pcm_filename = None - self.ytdl_file.delete() - - @classmethod - def create_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: - files = YtdlFile.download_from_url(url, **ytdl_args) - dfiles = [] - for file in files: - dfile = YtdlDiscord(file) - dfiles.append(dfile) - return dfiles - - @property - def info(self) -> typing.Optional[YtdlInfo]: - return self.ytdl_file.info diff --git a/royalnet/audio/ytdlfile.py b/royalnet/audio/ytdlfile.py deleted file mode 100644 index 0a851592..00000000 --- a/royalnet/audio/ytdlfile.py +++ /dev/null @@ -1,72 +0,0 @@ -import contextlib -import os -import typing -import youtube_dl -from .ytdlinfo import YtdlInfo -from .errors import NotFoundError, MultipleFilesError, MissingInfoError, AlreadyDownloadedError - - -class YtdlFile: - """Information about a youtube-dl downloaded file.""" - - _default_ytdl_args = { - "quiet": not __debug__, # Do not print messages to stdout. - "noplaylist": True, # Download single video instead of a playlist if in doubt. - "no_warnings": not __debug__, # Do not print out anything for warnings. - "outtmpl": "%(epoch)s-%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. - "ignoreerrors": True # Ignore unavailable videos - } - - def __init__(self, - url: str, - info: typing.Optional[YtdlInfo] = None, - filename: typing.Optional[str] = None): - self.url: str = url - self.info: typing.Optional[YtdlInfo] = info - self.filename: typing.Optional[str] = filename - - def has_info(self) -> bool: - return self.info is not None - - def is_downloaded(self) -> bool: - return self.filename is not None - - @contextlib.contextmanager - def open(self): - if not self.is_downloaded(): - raise FileNotFoundError("The file hasn't been downloaded yet.") - with open(self.filename, "r") as file: - yield file - - def update_info(self, **ytdl_args) -> None: - infos = YtdlInfo.retrieve_for_url(self.url, **ytdl_args) - if len(infos) == 0: - raise NotFoundError() - elif len(infos) > 1: - raise MultipleFilesError() - self.info = infos[0] - - def download_file(self, **ytdl_args) -> None: - if not self.has_info(): - raise MissingInfoError() - if self.is_downloaded(): - raise AlreadyDownloadedError() - with youtube_dl.YoutubeDL({**self._default_ytdl_args, **ytdl_args}) as ytdl: - filename = ytdl.prepare_filename(self.info.__dict__) - ytdl.download([self.info.webpage_url]) - self.filename = filename - - def delete(self): - if self.is_downloaded(): - os.remove(self.filename) - self.filename = None - - @classmethod - def download_from_url(cls, url: str, **ytdl_args) -> typing.List["YtdlFile"]: - infos = YtdlInfo.retrieve_for_url(url, **ytdl_args) - files = [] - for info in infos: - file = YtdlFile(url=info.webpage_url, info=info) - file.download_file(**ytdl_args) - files.append(file) - return files diff --git a/royalnet/audio/ytdlinfo.py b/royalnet/audio/ytdlinfo.py deleted file mode 100644 index cd08f6a5..00000000 --- a/royalnet/audio/ytdlinfo.py +++ /dev/null @@ -1,140 +0,0 @@ -import typing -import datetime -import dateparser -import youtube_dl -import discord -import royalnet.utils as u - - -class YtdlInfo: - """A wrapper around youtube_dl extracted info.""" - - _default_ytdl_args = { - "quiet": True, # Do not print messages to stdout. - "noplaylist": True, # Download single video instead of a playlist if in doubt. - "no_warnings": True, # Do not print out anything for warnings. - "outtmpl": "%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. - "ignoreerrors": True # Ignore unavailable videos - } - - def __init__(self, info: typing.Dict[str, typing.Any]): - """Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function. - - Warning: - Does not download the info, for that use :py:func:`royalnet.audio.YtdlInfo.retrieve_for_url`.""" - self.id: typing.Optional[str] = info.get("id") - self.uploader: typing.Optional[str] = info.get("uploader") - self.uploader_id: typing.Optional[str] = info.get("uploader_id") - self.uploader_url: typing.Optional[str] = info.get("uploader_url") - self.channel_id: typing.Optional[str] = info.get("channel_id") - self.channel_url: typing.Optional[str] = info.get("channel_url") - self.upload_date: typing.Optional[datetime.datetime] = dateparser.parse(u.ytdldateformat(info.get("upload_date"))) - self.license: typing.Optional[str] = info.get("license") - self.creator: typing.Optional[...] = info.get("creator") - self.title: typing.Optional[str] = info.get("title") - self.alt_title: typing.Optional[...] = info.get("alt_title") - self.thumbnail: typing.Optional[str] = info.get("thumbnail") - self.description: typing.Optional[str] = info.get("description") - self.categories: typing.Optional[typing.List[str]] = info.get("categories") - self.tags: typing.Optional[typing.List[str]] = info.get("tags") - self.subtitles: typing.Optional[typing.Dict[str, typing.List[typing.Dict[str, str]]]] = info.get("subtitles") - self.automatic_captions: typing.Optional[dict] = info.get("automatic_captions") - self.duration: typing.Optional[datetime.timedelta] = datetime.timedelta(seconds=info.get("duration", 0)) - self.age_limit: typing.Optional[int] = info.get("age_limit") - self.annotations: typing.Optional[...] = info.get("annotations") - self.chapters: typing.Optional[...] = info.get("chapters") - self.webpage_url: typing.Optional[str] = info.get("webpage_url") - self.view_count: typing.Optional[int] = info.get("view_count") - self.like_count: typing.Optional[int] = info.get("like_count") - self.dislike_count: typing.Optional[int] = info.get("dislike_count") - self.average_rating: typing.Optional[...] = info.get("average_rating") - self.formats: typing.Optional[list] = info.get("formats") - self.is_live: typing.Optional[bool] = info.get("is_live") - self.start_time: typing.Optional[float] = info.get("start_time") - self.end_time: typing.Optional[float] = info.get("end_time") - self.series: typing.Optional[str] = info.get("series") - self.season_number: typing.Optional[int] = info.get("season_number") - self.episode_number: typing.Optional[int] = info.get("episode_number") - self.track: typing.Optional[...] = info.get("track") - self.artist: typing.Optional[...] = info.get("artist") - self.extractor: typing.Optional[str] = info.get("extractor") - self.webpage_url_basename: typing.Optional[str] = info.get("webpage_url_basename") - self.extractor_key: typing.Optional[str] = info.get("extractor_key") - self.playlist: typing.Optional[str] = info.get("playlist") - self.playlist_index: typing.Optional[int] = info.get("playlist_index") - self.thumbnails: typing.Optional[typing.List[typing.Dict[str, str]]] = info.get("thumbnails") - self.display_id: typing.Optional[str] = info.get("display_id") - self.requested_subtitles: typing.Optional[...] = info.get("requested_subtitles") - self.requested_formats: typing.Optional[tuple] = info.get("requested_formats") - self.format: typing.Optional[str] = info.get("format") - self.format_id: typing.Optional[str] = info.get("format_id") - self.width: typing.Optional[int] = info.get("width") - self.height: typing.Optional[int] = info.get("height") - self.resolution: typing.Optional[...] = info.get("resolution") - self.fps: typing.Optional[int] = info.get("fps") - self.vcodec: typing.Optional[str] = info.get("vcodec") - self.vbr: typing.Optional[int] = info.get("vbr") - self.stretched_ratio: typing.Optional[...] = info.get("stretched_ratio") - self.acodec: typing.Optional[str] = info.get("acodec") - self.abr: typing.Optional[int] = info.get("abr") - self.ext: typing.Optional[str] = info.get("ext") - - @classmethod - def retrieve_for_url(cls, url, **ytdl_args) -> typing.List["YtdlInfo"]: - """Fetch the info for an url through YoutubeDL. - - Returns: - A :py:class:`list` containing the infos for the requested videos.""" - # So many redundant options! - ytdl = youtube_dl.YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) - first_info = ytdl.extract_info(url=url, download=False) - # No video was found - if first_info is None: - return [] - # If it is a playlist, create multiple videos! - if "entries" in first_info and first_info["entries"][0] is not None: - second_info_list = [] - for second_info in first_info["entries"]: - if second_info is None: - continue - second_info_list.append(YtdlInfo(second_info)) - return second_info_list - return [YtdlInfo(first_info)] - - def to_discord_embed(self) -> discord.Embed: - """Return this info as a :py:class:`discord.Embed`.""" - colors = { - "youtube": 0xCC0000, - "soundcloud": 0xFF5400, - "Clyp": 0x3DBEB3, - "Bandcamp": 0x1DA0C3, - "Peertube": 0x0A193C, - } - embed = discord.Embed(title=self.title, - colour=discord.Colour(colors.get(self.extractor, 0x4F545C)), - url=self.webpage_url if self.webpage_url is not None and self.webpage_url.startswith("http") else discord.embeds.EmptyEmbed) - if self.thumbnail: - embed.set_thumbnail(url=self.thumbnail) - if self.uploader: - embed.set_author(name=self.uploader, url=self.uploader_url if self.uploader_url is not None else discord.embeds.EmptyEmbed) - # embed.set_footer(text="Source: youtube-dl", icon_url="https://i.imgur.com/TSvSRYn.png") - if self.duration: - embed.add_field(name="Duration", value=str(self.duration), inline=True) - if self.upload_date: - embed.add_field(name="Published on", value=self.upload_date.strftime("%d %b %Y"), inline=True) - return embed - - def __repr__(self): - if self.title: - return f"" - if self.webpage_url: - return f"" - return f"" - - def __str__(self): - """Return the video name.""" - if self.title: - return self.title - if self.webpage_url: - return self.webpage_url - return self.id diff --git a/royalnet/audio/ytdlmp3.py b/royalnet/audio/ytdlmp3.py deleted file mode 100644 index f4607b5f..00000000 --- a/royalnet/audio/ytdlmp3.py +++ /dev/null @@ -1,66 +0,0 @@ -import typing -import re -import ffmpeg -import os -from .ytdlinfo import YtdlInfo -from .ytdlfile import YtdlFile -from .fileaudiosource import FileAudioSource - - -class YtdlMp3: - def __init__(self, ytdl_file: YtdlFile): - self.ytdl_file: YtdlFile = ytdl_file - self.mp3_filename: typing.Optional[str] = None - self._fas_spawned: typing.List[FileAudioSource] = [] - - def pcm_available(self): - return self.mp3_filename is not None and os.path.exists(self.mp3_filename) - - def convert_to_mp3(self) -> None: - if not self.ytdl_file.is_downloaded(): - raise FileNotFoundError("File hasn't been downloaded yet") - destination_filename = re.sub(r"\.[^.]+$", ".mp3", self.ytdl_file.filename) - out, err = ( - ffmpeg.input(self.ytdl_file.filename) - .output(destination_filename, format="mp3") - .overwrite_output() - .run_async() - ) - self.mp3_filename = destination_filename - - def ready_up(self): - if not self.ytdl_file.has_info(): - self.ytdl_file.update_info() - if not self.ytdl_file.is_downloaded(): - self.ytdl_file.download_file() - if not self.pcm_available(): - self.convert_to_mp3() - - def delete(self) -> None: - if self.pcm_available(): - for source in self._fas_spawned: - if not source.file.closed: - source.file.close() - os.remove(self.mp3_filename) - self.mp3_filename = None - self.ytdl_file.delete() - - @classmethod - def create_from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]: - files = YtdlFile.download_from_url(url, **ytdl_args) - dfiles = [] - for file in files: - dfile = YtdlMp3(file) - dfiles.append(dfile) - return dfiles - - @classmethod - def create_and_ready_from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]: - dfiles = cls.create_from_url(url, **ytdl_args) - for dfile in dfiles: - dfile.ready_up() - return dfiles - - @property - def info(self) -> typing.Optional[YtdlInfo]: - return self.ytdl_file.info diff --git a/royalnet/backpack/README.md b/royalnet/backpack/README.md new file mode 100644 index 00000000..0121370f --- /dev/null +++ b/royalnet/backpack/README.md @@ -0,0 +1,3 @@ +# `backpack` + +A Pack that is imported by default by all `royalnet` instances. diff --git a/royalnet/packs/common/__init__.py b/royalnet/backpack/__init__.py similarity index 58% rename from royalnet/packs/common/__init__.py rename to royalnet/backpack/__init__.py index feb329ab..0e9f6c05 100644 --- a/royalnet/packs/common/__init__.py +++ b/royalnet/backpack/__init__.py @@ -1,16 +1,21 @@ -# This is a template Pack __init__. You can use this without changing anything in other packages too! +"""A Pack that is imported by default by all Royalnet instances. -from . import commands, tables, stars +Keep things here to a minimum!""" + +from . import commands, tables, stars, events from .commands import available_commands from .tables import available_tables from .stars import available_page_stars, available_exception_stars +from .events import available_events __all__ = [ "commands", "tables", "stars", + "events", "available_commands", "available_tables", "available_page_stars", "available_exception_stars", + "available_events", ] diff --git a/royalnet/packs/common/commands/__init__.py b/royalnet/backpack/commands/__init__.py similarity index 63% rename from royalnet/packs/common/commands/__init__.py rename to royalnet/backpack/commands/__init__.py index 39e16489..1211e4eb 100644 --- a/royalnet/packs/common/commands/__init__.py +++ b/royalnet/backpack/commands/__init__.py @@ -1,11 +1,13 @@ # Imports go here! -from .ping import PingCommand from .version import VersionCommand +from .exception import ExceptionCommand +from .excevent import ExceventCommand # Enter the commands of your Pack here! available_commands = [ - PingCommand, - VersionCommand + VersionCommand, + ExceptionCommand, + ExceventCommand, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/commands/exception.py b/royalnet/backpack/commands/exception.py new file mode 100644 index 00000000..1e91ea7c --- /dev/null +++ b/royalnet/backpack/commands/exception.py @@ -0,0 +1,13 @@ +import royalnet +from royalnet.commands import * + + +class ExceptionCommand(Command): + name: str = "exception" + + description: str = "Raise an exception in the command." + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if not self.interface.cfg["exc_debug"]: + raise UserError(f"{self.interface.prefix}{self.name} is not enabled.") + raise Exception(f"{self.interface.prefix}{self.name} was called") diff --git a/royalnet/backpack/commands/excevent.py b/royalnet/backpack/commands/excevent.py new file mode 100644 index 00000000..0535edc2 --- /dev/null +++ b/royalnet/backpack/commands/excevent.py @@ -0,0 +1,14 @@ +import royalnet +from royalnet.commands import * + + +class ExceventCommand(Command): + name: str = "excevent" + + description: str = "Call an event that raises an exception." + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if not self.interface.cfg["exc_debug"]: + raise UserError(f"{self.interface.prefix}{self.name} is not enabled.") + await self.interface.call_herald_event(self.interface.name, "exception") + await data.reply("✅ Event called!") diff --git a/royalnet/backpack/commands/version.py b/royalnet/backpack/commands/version.py new file mode 100644 index 00000000..fee185a2 --- /dev/null +++ b/royalnet/backpack/commands/version.py @@ -0,0 +1,18 @@ +import royalnet +from royalnet.commands import * + + +class VersionCommand(Command): + name: str = "version" + + description: str = "Display the current Royalnet version." + + async def run(self, args: CommandArgs, data: CommandData) -> None: + # noinspection PyUnreachableCode + if __debug__: + message = f"â„¹ï¸ Royalnet {royalnet.__version__} (debug)\n" + else: + message = f"â„¹ï¸ Royalnet {royalnet.__version__}\n" + if "69" in message: + message += "(Nice.)" + await data.reply(message) diff --git a/royalnet/backpack/events/__init__.py b/royalnet/backpack/events/__init__.py new file mode 100644 index 00000000..eac5e669 --- /dev/null +++ b/royalnet/backpack/events/__init__.py @@ -0,0 +1,14 @@ +# Imports go here! +from .exception import ExceptionEvent + +# Enter the commands of your Pack here! +available_events = [ + +] + +# noinspection PyUnreachableCode +if __debug__: + available_events.append(ExceptionEvent) + +# Don't change this, it should automatically generate __all__ +__all__ = [command.__name__ for command in available_events] diff --git a/royalnet/backpack/events/exception.py b/royalnet/backpack/events/exception.py new file mode 100644 index 00000000..b9cedc52 --- /dev/null +++ b/royalnet/backpack/events/exception.py @@ -0,0 +1,10 @@ +from royalnet.commands import * + + +class ExceptionEvent(Event): + name = "exception" + + def run(self, **kwargs): + if not self.interface.cfg["exc_debug"]: + raise UserError(f"{self.interface.prefix}{self.name} is not enabled.") + raise Exception(f"{self.name} event was called") diff --git a/royalnet/packs/common/stars/__init__.py b/royalnet/backpack/stars/__init__.py similarity index 100% rename from royalnet/packs/common/stars/__init__.py rename to royalnet/backpack/stars/__init__.py diff --git a/royalnet/packs/common/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py similarity index 64% rename from royalnet/packs/common/stars/api_royalnet_version.py rename to royalnet/backpack/stars/api_royalnet_version.py index 37e9e487..d9a3f4f7 100644 --- a/royalnet/packs/common/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,15 +1,18 @@ import royalnet from starlette.requests import Request from starlette.responses import * -from royalnet.web import PageStar +from royalnet.constellation import PageStar +from ..tables import available_tables class ApiRoyalnetVersionStar(PageStar): path = "/api/royalnet/version" + tables = set(available_tables) + async def page(self, request: Request) -> JSONResponse: return JSONResponse({ "version": { - "semantic": royalnet.version.semantic + "semantic": royalnet.__version__, } }) diff --git a/royalnet/packs/common/tables/__init__.py b/royalnet/backpack/tables/__init__.py similarity index 92% rename from royalnet/packs/common/tables/__init__.py rename to royalnet/backpack/tables/__init__.py index cc96674c..ca4f3429 100644 --- a/royalnet/packs/common/tables/__init__.py +++ b/royalnet/backpack/tables/__init__.py @@ -4,11 +4,11 @@ from .telegram import Telegram from .discord import Discord # Enter the tables of your Pack here! -available_tables = [ +available_tables = { User, Telegram, Discord -] +} # Don't change this, it should automatically generate __all__ __all__ = [table.__name__ for table in available_tables] diff --git a/royalnet/packs/common/tables/discord.py b/royalnet/backpack/tables/discord.py similarity index 100% rename from royalnet/packs/common/tables/discord.py rename to royalnet/backpack/tables/discord.py diff --git a/royalnet/packs/common/tables/telegram.py b/royalnet/backpack/tables/telegram.py similarity index 100% rename from royalnet/packs/common/tables/telegram.py rename to royalnet/backpack/tables/telegram.py diff --git a/royalnet/packs/common/tables/users.py b/royalnet/backpack/tables/users.py similarity index 100% rename from royalnet/packs/common/tables/users.py rename to royalnet/backpack/tables/users.py diff --git a/royalnet/backpack/utils/__init__.py b/royalnet/backpack/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/royalnet/bard/__init__.py b/royalnet/bard/__init__.py new file mode 100644 index 00000000..5ead8635 --- /dev/null +++ b/royalnet/bard/__init__.py @@ -0,0 +1,18 @@ +from .ytdlinfo import YtdlInfo +from .ytdlfile import YtdlFile +from .ytdlmp3 import YtdlMp3 +from .ytdldiscord import YtdlDiscord + +try: + from .fileaudiosource import FileAudioSource +except ImportError: + FileAudioSource = None + + +__all__ = [ + "YtdlInfo", + "YtdlFile", + "YtdlMp3", + "YtdlDiscord", + "FileAudioSource", +] diff --git a/royalnet/bard/errors.py b/royalnet/bard/errors.py new file mode 100644 index 00000000..1368a7cb --- /dev/null +++ b/royalnet/bard/errors.py @@ -0,0 +1,14 @@ +class BardError(Exception): + """Base class for :mod:`bard` errors.""" + + +class YtdlError(BardError): + """Base class for errors caused by :mod:`youtube_dl`.""" + + +class NotFoundError(YtdlError): + """The requested resource wasn't found.""" + + +class MultipleFilesError(YtdlError): + """The resource contains multiple media files.""" diff --git a/royalnet/bard/ytdldiscord.py b/royalnet/bard/ytdldiscord.py new file mode 100644 index 00000000..3fc80d9b --- /dev/null +++ b/royalnet/bard/ytdldiscord.py @@ -0,0 +1,112 @@ +import typing +import re +import os +import logging +from contextlib import asynccontextmanager +from royalnet.utils import asyncify, MultiLock, FileAudioSource +from royalnet.bard import YtdlInfo, YtdlFile + +try: + import ffmpeg +except ImportError: + ffmpeg = None + +try: + import discord +except ImportError: + discord = None + +log = logging.getLogger(__name__) + + +class YtdlDiscord: + """A representation of a :class:`YtdlFile` conversion to the :mod:`discord` PCM format.""" + + def __init__(self, ytdl_file: YtdlFile): + self.ytdl_file: YtdlFile = ytdl_file + self.pcm_filename: typing.Optional[str] = None + self.lock: MultiLock = MultiLock() + + @property + def is_converted(self): + """Has the file been converted?""" + return self.pcm_filename is not None + + async def convert_to_pcm(self) -> None: + """Convert the file to pcm with :mod:`ffmpeg`.""" + if ffmpeg is None: + raise ImportError("'bard' extra is not installed") + await self.ytdl_file.download_file() + if self.pcm_filename is None: + async with self.ytdl_file.lock.normal(): + destination_filename = re.sub(r"\.[^.]+$", ".pcm", self.ytdl_file.filename) + async with self.lock.exclusive(): + log.debug(f"Converting to PCM: {self.ytdl_file.filename}") + await asyncify( + ffmpeg.input(self.ytdl_file.filename) + .output(destination_filename, format="s16le", ac=2, ar="48000") + .overwrite_output() + .run + ) + self.pcm_filename = destination_filename + + async def delete_asap(self) -> None: + """Delete the mp3 file.""" + log.debug(f"Trying to delete: {self}") + if self.is_converted: + async with self.lock.exclusive(): + os.remove(self.pcm_filename) + log.debug(f"Deleted: {self.pcm_filename}") + self.pcm_filename = None + + @classmethod + async def from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: + """Create a :class:`list` of :class:`YtdlMp3` from a URL.""" + files = await YtdlFile.from_url(url, **ytdl_args) + dfiles = [] + for file in files: + dfile = YtdlDiscord(file) + dfiles.append(dfile) + return dfiles + + @property + def info(self) -> typing.Optional[YtdlInfo]: + """Shortcut to get the :class:`YtdlInfo` of the object.""" + return self.ytdl_file.info + + @asynccontextmanager + async def spawn_audiosource(self): + log.debug(f"Spawning audio_source for: {self}") + if FileAudioSource is None: + raise ImportError("'discord' extra is not installed") + await self.convert_to_pcm() + async with self.lock.normal(): + with open(self.pcm_filename, "rb") as stream: + fas = FileAudioSource(stream) + yield fas + + def embed(self) -> "discord.Embed": + """Return this info as a :py:class:`discord.Embed`.""" + if discord is None: + raise ImportError("'discord' extra is not installed") + colors = { + "youtube": 0xCC0000, + "soundcloud": 0xFF5400, + "Clyp": 0x3DBEB3, + "Bandcamp": 0x1DA0C3, + "PeerTube": 0xF1680D, + } + embed = discord.Embed(title=self.info.title, + colour=discord.Colour(colors.get(self.info.extractor, 0x4F545C)), + url=self.info.webpage_url if (self.info.webpage_url and self.info.webpage_url.startswith("http")) else discord.embeds.EmptyEmbed) + if self.info.thumbnail: + embed.set_thumbnail(url=self.info.thumbnail) + if self.info.uploader: + embed.set_author(name=self.info.uploader, + url=self.info.uploader_url if self.info.uploader_url is not None else discord.embeds.EmptyEmbed) + # embed.set_footer(text="Source: youtube-dl", icon_url="https://i.imgur.com/TSvSRYn.png") + if self.info.duration: + embed.add_field(name="Duration", value=str(self.info.duration), inline=True) + if self.info.upload_date: + embed.add_field(name="Published on", value=self.info.upload_date.strftime("%d %b %Y"), inline=True) + return embed diff --git a/royalnet/bard/ytdlfile.py b/royalnet/bard/ytdlfile.py new file mode 100644 index 00000000..432c10cc --- /dev/null +++ b/royalnet/bard/ytdlfile.py @@ -0,0 +1,122 @@ +import os +import logging +from contextlib import asynccontextmanager +from typing import Optional, List, Dict, Any +from royalnet.utils import asyncify, MultiLock +from asyncio import AbstractEventLoop, get_event_loop +from .ytdlinfo import YtdlInfo +from .errors import NotFoundError, MultipleFilesError + +try: + from youtube_dl import YoutubeDL +except ImportError: + YoutubeDL = None + + +log = logging.getLogger(__name__) + + +class YtdlFile: + """A representation of a file download with `youtube_dl `_.""" + + default_ytdl_args = { + "quiet": not __debug__, # Do not print messages to stdout. + "noplaylist": True, # Download single video instead of a playlist if in doubt. + "no_warnings": not __debug__, # Do not print out anything for warnings. + "outtmpl": "%(epoch)s-%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. + "ignoreerrors": True # Ignore unavailable videos + } + + def __init__(self, + url: str, + info: Optional[YtdlInfo] = None, + filename: Optional[str] = None, + ytdl_args: Optional[Dict[str, Any]] = None, + loop: Optional[AbstractEventLoop] = None): + """Create a :class:`YtdlFile` instance. + + Warning: + Please avoid using directly :meth:`.__init__`, use :meth:`.from_url` instead!""" + self.url: str = url + self.info: Optional[YtdlInfo] = info + self.filename: Optional[str] = filename + self.ytdl_args: Dict[str, Any] = {**self.default_ytdl_args, **ytdl_args} + self.lock: MultiLock = MultiLock() + if not loop: + loop = get_event_loop() + self._loop = loop + + @property + def has_info(self) -> bool: + """Does the :class:`YtdlFile` have info available?""" + return self.info is not None + + async def retrieve_info(self) -> None: + """Retrieve info about the :class:`YtdlFile` through :class:`YoutubeDL`.""" + if not self.has_info: + infos = await asyncify(YtdlInfo.from_url, self.url, loop=self._loop, **self.ytdl_args) + if len(infos) == 0: + raise NotFoundError() + elif len(infos) > 1: + raise MultipleFilesError() + self.info = infos[0] + + @property + def is_downloaded(self) -> bool: + """Has the file been downloaded yet?""" + return self.filename is not None + + async def download_file(self) -> None: + """Download the file.""" + if YoutubeDL is None: + raise ImportError("'bard' extra is not installed") + + def download(): + """Download function block to be asyncified.""" + with YoutubeDL(self.ytdl_args) as ytdl: + filename = ytdl.prepare_filename(self.info.__dict__) + with YoutubeDL({**self.ytdl_args, "outtmpl": filename}) as ytdl: + ytdl.download([self.info.webpage_url]) + self.filename = filename + + await self.retrieve_info() + async with self.lock.exclusive(): + log.debug(f"Downloading with youtube-dl: {self}") + await asyncify(download, loop=self._loop) + + @asynccontextmanager + async def aopen(self): + """Open the downloaded file as an async context manager (and download it if it isn't available yet). + + Example: + You can use the async context manager like this: :: + + async with ytdlfile.aopen() as file: + b: bytes = file.read() + + """ + await self.download_file() + async with self.lock.normal(): + log.debug(f"File opened: {self.filename}") + with open(self.filename, "rb") as file: + yield file + log.debug(f"File closed: {self.filename}") + + async def delete_asap(self): + """As soon as nothing is using the file, delete it.""" + log.debug(f"Trying to delete: {self.filename}") + if self.filename is not None: + async with self.lock.exclusive(): + os.remove(self.filename) + log.debug(f"Deleted: {self.filename}") + self.filename = None + + @classmethod + async def from_url(cls, url: str, **ytdl_args) -> List["YtdlFile"]: + """Create a :class:`list` of :class:`YtdlFile` from a URL.""" + infos = await YtdlInfo.from_url(url, **ytdl_args) + files = [] + for info in infos: + file = YtdlFile(url=info.webpage_url, info=info, ytdl_args=ytdl_args) + files.append(file) + return files diff --git a/royalnet/bard/ytdlinfo.py b/royalnet/bard/ytdlinfo.py new file mode 100644 index 00000000..1ebcafda --- /dev/null +++ b/royalnet/bard/ytdlinfo.py @@ -0,0 +1,132 @@ +from asyncio import AbstractEventLoop, get_event_loop +from typing import Optional, Dict, List, Any +from datetime import datetime, timedelta +import dateparser +import logging +from royalnet.utils import ytdldateformat, asyncify + +try: + from youtube_dl import YoutubeDL +except ImportError: + YoutubeDL = None + + +log = logging.getLogger(__name__) + + +class YtdlInfo: + """A wrapper around `youtube_dl `_ extracted info.""" + + _default_ytdl_args = { + "quiet": True, # Do not print messages to stdout. + "noplaylist": True, # Download single video instead of a playlist if in doubt. + "no_warnings": True, # Do not print out anything for warnings. + "outtmpl": "%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. + "ignoreerrors": True # Ignore unavailable videos + } + + def __init__(self, info: Dict[str, Any]): + """Create a :class:`YtdlInfo` from the dict returned by the :func:`YoutubeDL.extract_info` function. + + Warning: + Does not download the info, to do that use :func:`.retrieve_for_url`.""" + self.id: Optional[str] = info.get("id") + self.uploader: Optional[str] = info.get("uploader") + self.uploader_id: Optional[str] = info.get("uploader_id") + self.uploader_url: Optional[str] = info.get("uploader_url") + self.channel_id: Optional[str] = info.get("channel_id") + self.channel_url: Optional[str] = info.get("channel_url") + self.upload_date: Optional[datetime] = dateparser.parse(ytdldateformat(info.get("upload_date"))) + self.license: Optional[str] = info.get("license") + self.creator: Optional[...] = info.get("creator") + self.title: Optional[str] = info.get("title") + self.alt_title: Optional[...] = info.get("alt_title") + self.thumbnail: Optional[str] = info.get("thumbnail") + self.description: Optional[str] = info.get("description") + self.categories: Optional[List[str]] = info.get("categories") + self.tags: Optional[List[str]] = info.get("tags") + self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles") + self.automatic_captions: Optional[dict] = info.get("automatic_captions") + self.duration: Optional[timedelta] = timedelta(seconds=info.get("duration", 0)) + self.age_limit: Optional[int] = info.get("age_limit") + self.annotations: Optional[...] = info.get("annotations") + self.chapters: Optional[...] = info.get("chapters") + self.webpage_url: Optional[str] = info.get("webpage_url") + self.view_count: Optional[int] = info.get("view_count") + self.like_count: Optional[int] = info.get("like_count") + self.dislike_count: Optional[int] = info.get("dislike_count") + self.average_rating: Optional[...] = info.get("average_rating") + self.formats: Optional[list] = info.get("formats") + self.is_live: Optional[bool] = info.get("is_live") + self.start_time: Optional[float] = info.get("start_time") + self.end_time: Optional[float] = info.get("end_time") + self.series: Optional[str] = info.get("series") + self.season_number: Optional[int] = info.get("season_number") + self.episode_number: Optional[int] = info.get("episode_number") + self.track: Optional[...] = info.get("track") + self.artist: Optional[...] = info.get("artist") + self.extractor: Optional[str] = info.get("extractor") + self.webpage_url_basename: Optional[str] = info.get("webpage_url_basename") + self.extractor_key: Optional[str] = info.get("extractor_key") + self.playlist: Optional[str] = info.get("playlist") + self.playlist_index: Optional[int] = info.get("playlist_index") + self.thumbnails: Optional[List[Dict[str, str]]] = info.get("thumbnails") + self.display_id: Optional[str] = info.get("display_id") + self.requested_subtitles: Optional[...] = info.get("requested_subtitles") + self.requested_formats: Optional[tuple] = info.get("requested_formats") + self.format: Optional[str] = info.get("format") + self.format_id: Optional[str] = info.get("format_id") + self.width: Optional[int] = info.get("width") + self.height: Optional[int] = info.get("height") + self.resolution: Optional[...] = info.get("resolution") + self.fps: Optional[int] = info.get("fps") + self.vcodec: Optional[str] = info.get("vcodec") + self.vbr: Optional[int] = info.get("vbr") + self.stretched_ratio: Optional[...] = info.get("stretched_ratio") + self.acodec: Optional[str] = info.get("acodec") + self.abr: Optional[int] = info.get("abr") + self.ext: Optional[str] = info.get("ext") + + @classmethod + async def from_url(cls, url, loop: Optional[AbstractEventLoop] = None, **ytdl_args) -> List["YtdlInfo"]: + """Fetch the info for an url through :class:`YoutubeDL`. + + Returns: + A :class:`list` containing the infos for the requested videos.""" + if YoutubeDL is None: + raise ImportError("'bard' extra is not installed") + + if loop is None: + loop: AbstractEventLoop = get_event_loop() + # So many redundant options! + log.debug(f"Fetching info: {url}") + with YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl: + first_info = await asyncify(ytdl.extract_info, loop=loop, url=url, download=False) + # No video was found + if first_info is None: + return [] + # If it is a playlist, create multiple videos! + if "entries" in first_info and first_info["entries"][0] is not None: + log.debug(f"Found a playlist: {url}") + second_info_list = [] + for second_info in first_info["entries"]: + if second_info is None: + continue + second_info_list.append(YtdlInfo(second_info)) + return second_info_list + log.debug(f"Found a single video: {url}") + return [YtdlInfo(first_info)] + + def __repr__(self): + if self.title: + return f"" + if self.webpage_url: + return f"" + return f"" + + def __str__(self): + if self.title: + return self.title + if self.webpage_url: + return self.webpage_url + return self.id diff --git a/royalnet/bard/ytdlmp3.py b/royalnet/bard/ytdlmp3.py new file mode 100644 index 00000000..1a76560a --- /dev/null +++ b/royalnet/bard/ytdlmp3.py @@ -0,0 +1,63 @@ +import typing +import re +import os +from royalnet.utils import asyncify, MultiLock +from .ytdlinfo import YtdlInfo +from .ytdlfile import YtdlFile + +try: + import ffmpeg +except ImportError: + ffmpeg = None + + +class YtdlMp3: + """A representation of a :class:`YtdlFile` conversion to mp3.""" + def __init__(self, ytdl_file: YtdlFile): + self.ytdl_file: YtdlFile = ytdl_file + self.mp3_filename: typing.Optional[str] = None + self.lock: MultiLock = MultiLock() + + @property + def is_converted(self): + """Has the file been converted?""" + return self.mp3_filename is not None + + async def convert_to_mp3(self) -> None: + """Convert the file to mp3 with :mod:`ffmpeg`.""" + if ffmpeg is None: + raise ImportError("'bard' extra is not installed") + await self.ytdl_file.download_file() + if self.mp3_filename is None: + async with self.ytdl_file.lock.normal(): + destination_filename = re.sub(r"\.[^.]+$", ".mp3", self.ytdl_file.filename) + async with self.lock.exclusive(): + await asyncify( + ffmpeg.input(self.ytdl_file.filename) + .output(destination_filename, format="mp3") + .overwrite_output() + .run + ) + self.mp3_filename = destination_filename + + async def delete_asap(self) -> None: + """Delete the mp3 file.""" + if self.is_converted: + async with self.lock.exclusive(): + os.remove(self.mp3_filename) + self.mp3_filename = None + + @classmethod + async def from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]: + """Create a :class:`list` of :class:`YtdlMp3` from a URL.""" + files = await YtdlFile.from_url(url, **ytdl_args) + dfiles = [] + for file in files: + dfile = YtdlMp3(file) + dfiles.append(dfile) + return dfiles + + @property + def info(self) -> typing.Optional[YtdlInfo]: + """Shortcut to get the :class:`YtdlInfo` of the object.""" + return self.ytdl_file.info diff --git a/royalnet/bots/__init__.py b/royalnet/bots/__init__.py deleted file mode 100644 index a6aef182..00000000 --- a/royalnet/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Various bot interfaces, and a common class to create new ones.""" - -from .generic import GenericBot -from .telegram import TelegramBot -from .discord import DiscordBot - -__all__ = ["TelegramBot", "DiscordBot", "GenericBot"] diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py deleted file mode 100644 index 1fc21de2..00000000 --- a/royalnet/bots/discord.py +++ /dev/null @@ -1,272 +0,0 @@ -import discord -import sentry_sdk -import logging as _logging -from .generic import GenericBot -from ..utils import * -from ..error import * -from ..audio import * -from ..commands import * - - -log = _logging.getLogger(__name__) - - -class MusicData: - def __init__(self): - self.playmode: playmodes.PlayMode = playmodes.Playlist() - self.voice_client: typing.Optional[discord.VoiceClient] = None - - def queue_preview(self): - return self.playmode.queue_preview() - - -class DiscordBot(GenericBot): - """A bot that connects to `Discord `_.""" - interface_name = "discord" - - def _init_voice(self): - """Initialize the variables needed for the connection to voice chat.""" - log.debug(f"Creating music_data dict") - self.music_data: typing.Dict[discord.Guild, MusicData] = {} - - def _interface_factory(self) -> typing.Type[CommandInterface]: - # noinspection PyPep8Naming - GenericInterface = super()._interface_factory() - - # noinspection PyMethodParameters,PyAbstractClass - class DiscordInterface(GenericInterface): - name = self.interface_name - prefix = "!" - - return DiscordInterface - - def _data_factory(self) -> typing.Type[CommandData]: - # noinspection PyMethodParameters,PyAbstractClass - class DiscordData(CommandData): - def __init__(data, interface: CommandInterface, message: discord.Message): - super().__init__(interface) - data.message = message - - async def reply(data, text: str): - await data.message.channel.send(discord_escape(text)) - - async def get_author(data, error_if_none=False): - user: discord.Member = data.message.author - query = data.session.query(self.master_table) - for link in self.identity_chain: - query = query.join(link.mapper.class_) - query = query.filter(self.identity_column == user.id) - result = await asyncify(query.one_or_none) - if result is None and error_if_none: - raise CommandError("You must be registered to use this command.") - return result - - async def delete_invoking(data, error_if_unavailable=False): - await data.message.delete() - - return DiscordData - - def _bot_factory(self) -> typing.Type[discord.Client]: - """Create a custom DiscordClient class inheriting from :py:class:`discord.Client`.""" - log.debug(f"Creating DiscordClient") - - # noinspection PyMethodParameters - class DiscordClient(discord.Client): - async def vc_connect_or_move(cli, channel: discord.VoiceChannel): - music_data = self.music_data.get(channel.guild) - if music_data is None: - # Create a MusicData object - music_data = MusicData() - self.music_data[channel.guild] = music_data - # Connect to voice - log.debug(f"Connecting to Voice in {channel}") - try: - music_data.voice_client = await channel.connect(reconnect=False, timeout=10) - except Exception: - log.warning(f"Failed to connect to Voice in {channel}") - del self.music_data[channel.guild] - raise - else: - log.debug(f"Connected to Voice in {channel}") - else: - if music_data.voice_client is None: - # TODO: change exception type - raise Exception("Another connection attempt is already in progress.") - # Try to move to a different channel - voice_client = music_data.voice_client - log.debug(f"Moving {voice_client} to {channel}") - await voice_client.move_to(channel) - log.debug(f"Moved {voice_client} to {channel}") - - async def on_message(cli, message: discord.Message): - self.loop.create_task(cli._handle_message(message)) - - async def _handle_message(cli, message: discord.Message): - text = message.content - # Skip non-text messages - if not text: - return - # Skip non-command updates - if not text.startswith("!"): - return - # Skip bot messages - author: typing.Union[discord.User] = message.author - if author.bot: - return - # Find and clean parameters - command_text, *parameters = text.split(" ") - # Don't use a case-sensitive command name - command_name = command_text.lower() - # Find the command - try: - command = self.commands[command_name] - except KeyError: - # Skip the message - return - # Prepare data - data = self._Data(interface=command.interface, message=message) - # Call the command - log.debug(f"Calling command '{command.name}'") - with message.channel.typing(): - # Run the command - try: - await command.run(CommandArgs(parameters), data) - except InvalidInputError as e: - await data.reply(f":warning: {e.message}\n" - f"Syntax: [c]/{command.name} {command.syntax}[/c]") - except UnsupportedError as e: - await data.reply(f":warning: {e.message}") - except CommandError as e: - await data.reply(f":warning: {e.message}") - except Exception as e: - sentry_sdk.capture_exception(e) - error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" - error_message += '\n'.join(e.args) - await data.reply(error_message) - # Close the data session - await data.session_close() - - async def on_connect(cli): - log.debug("Connected to Discord") - - async def on_disconnect(cli): - log.error("Disconnected from Discord!") - - async def on_ready(cli) -> None: - log.debug("Connection successful, client is ready") - await cli.change_presence(status=discord.Status.online) - - def find_guild_by_name(cli, name: str) -> typing.List[discord.Guild]: - """Find the :py:class:`discord.Guild` with the specified name (case insensitive).""" - all_guilds: typing.List[discord.Guild] = cli.guilds - matching_channels: typing.List[discord.Guild] = [] - for guild in all_guilds: - if guild.name.lower() == name.lower(): - matching_channels.append(guild) - return matching_channels - - def find_channel_by_name(cli, - name: str, - guild: typing.Optional[discord.Guild] = None) -> typing.List[discord.abc.GuildChannel]: - """Find the :py:class:`TextChannel`, :py:class:`VoiceChannel` or :py:class:`CategoryChannel` with the - specified name (case insensitive). - - You can specify a guild to only find channels in that specific guild.""" - if guild is not None: - all_channels = guild.channels - else: - all_channels: typing.List[discord.abc.GuildChannel] = cli.get_all_channels() - matching_channels: typing.List[discord.abc.GuildChannel] = [] - for channel in all_channels: - if not (isinstance(channel, discord.TextChannel) - or isinstance(channel, discord.VoiceChannel) - or isinstance(channel, discord.CategoryChannel)): - continue - if channel.name.lower() == name.lower(): - matching_channels.append(channel) - return matching_channels - - def find_voice_client_by_guild(cli, guild: discord.Guild) -> typing.Optional[discord.VoiceClient]: - """Find the :py:class:`discord.VoiceClient` belonging to a specific :py:class:`discord.Guild`.""" - for voice_client in cli.voice_clients: - if voice_client.guild == guild: - return voice_client - return None - - return DiscordClient - - def _init_client(self): - """Create an instance of the DiscordClient class created in :py:func:`royalnet.bots.DiscordBot._bot_factory`.""" - log.debug(f"Creating DiscordClient instance") - self._Client = self._bot_factory() - self.client = self._Client() - - def _initialize(self): - super()._initialize() - self._init_client() - self._init_voice() - - async def run(self): - """Login to Discord, then run the bot.""" - if not self.initialized: - self._initialize() - log.debug("Getting Discord secret") - token = self.get_secret("discord") - log.info(f"Logging in to Discord") - await self.client.login(token) - log.info(f"Connecting to Discord") - await self.client.connect() - - async def add_to_music_data(self, dfiles: typing.List[YtdlDiscord], guild: discord.Guild): - """Add a list of :py:class:`royalnet.audio.YtdlDiscord` to the corresponding music_data object.""" - guild_music_data = self.music_data[guild] - if guild_music_data is None: - raise CommandError(f"No music_data has been created for guild {guild}") - for dfile in dfiles: - log.debug(f"Adding {dfile} to music_data") - await asyncify(dfile.ready_up) - guild_music_data.playmode.add(dfile) - if guild_music_data.playmode.now_playing is None: - await self.advance_music_data(guild) - - async def advance_music_data(self, guild: discord.Guild): - """Try to play the next song, while it exists. Otherwise, just return.""" - guild_music_data: MusicData = self.music_data[guild] - voice_client: discord.VoiceClient = guild_music_data.voice_client - next_source: discord.AudioSource = await guild_music_data.playmode.next() - await self.update_activity_with_source_title() - if next_source is None: - log.debug(f"Ending playback chain") - return - - def advance(error=None): - if error: - voice_client.disconnect(force=True) - guild_music_data.voice_client = None - log.error(f"Error while advancing music_data: {error}") - return - self.loop.create_task(self.advance_music_data(guild)) - - log.debug(f"Starting playback of {next_source}") - voice_client.play(next_source, after=advance) - - async def update_activity_with_source_title(self): - """Change the bot's presence (using :py:func:`discord.Client.change_presence`) to match the current listening status. - - If multiple guilds are using the bot, the bot will always have an empty presence.""" - if len(self.music_data) != 1: - # Multiple guilds are using the bot, do not display anything - log.debug(f"Updating current Activity: setting to None, as multiple guilds are using the bot") - await self.client.change_presence(status=discord.Status.online) - return - play_mode: playmodes.PlayMode = self.music_data[list(self.music_data)[0]].playmode - now_playing = play_mode.now_playing - if now_playing is None: - # No songs are playing now - log.debug(f"Updating current Activity: setting to None, as nothing is currently being played") - await self.client.change_presence(status=discord.Status.online) - return - log.debug(f"Updating current Activity: listening to {now_playing.info.title}") - await self.client.change_presence(activity=discord.Activity(name=now_playing.info.title, - type=discord.ActivityType.listening), - status=discord.Status.online) diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py deleted file mode 100644 index 4014e9fa..00000000 --- a/royalnet/bots/generic.py +++ /dev/null @@ -1,219 +0,0 @@ -import sys -import asyncio -import logging -import sentry_sdk -import keyring -import royalnet.version -import royalherald as rh -from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration -from sentry_sdk.integrations.aiohttp import AioHttpIntegration -from sentry_sdk.integrations.logging import LoggingIntegration -from ..utils import * -from ..database import * -from ..commands import * -from ..error import * - - -log = logging.getLogger(__name__) - - -class GenericBot: - """A common bot class, to be used as base for the other more specific classes, such as - :py:class:`royalnet.bots.TelegramBot` and :py:class:`royalnet.bots.DiscordBot`. """ - interface_name = NotImplemented - - def _init_commands(self) -> None: - """Generate the ``packs`` dictionary required to handle incoming messages, and the ``network_handlers`` - dictionary required to handle incoming requests. """ - log.info(f"Registering packs...") - self._Interface = self._interface_factory() - self._Data = self._data_factory() - self.commands = {} - self.network_handlers: typing.Dict[str, typing.Callable[["GenericBot", typing.Any], - typing.Awaitable[typing.Optional[typing.Dict]]]] = {} - for SelectedCommand in self.uninitialized_commands: - interface = self._Interface() - try: - command = SelectedCommand(interface) - except Exception as e: - log.error(f"{e.__class__.__qualname__} during the registration of {SelectedCommand.__qualname__}") - sentry_sdk.capture_exception(e) - continue - # Linking the command to the interface - interface.command = command - # Override the main command name, but warn if it's overriding something - if f"{interface.prefix}{SelectedCommand.name}" in self.commands: - log.warning(f"Overriding (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}") - else: - log.debug(f"Registering: {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}") - self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command - # Register aliases, but don't override anything - for alias in SelectedCommand.aliases: - if f"{interface.prefix}{alias}" not in self.commands: - log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") - self.commands[f"{interface.prefix}{alias}"] = self.commands[f"{interface.prefix}{SelectedCommand.name}"] - else: - log.info(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") - - def _interface_factory(self) -> typing.Type[CommandInterface]: - # noinspection PyAbstractClass,PyMethodParameters - class GenericInterface(CommandInterface): - alchemy = self.alchemy - bot = self - loop = self.loop - - def register_herald_action(ci, - event_name: str, - coroutine: typing.Callable[[typing.Any], typing.Awaitable[typing.Dict]]) -> None: - self.network_handlers[event_name] = coroutine - - def unregister_herald_action(ci, event_name: str): - del self.network_handlers[event_name] - - async def call_herald_action(ci, destination: str, event_name: str, args: typing.Dict) -> typing.Dict: - if self.network is None: - raise UnsupportedError("Herald is not enabled on this bot") - request: rh.Request = rh.Request(handler=event_name, data=args) - response: rh.Response = await self.network.request(destination=destination, request=request) - if isinstance(response, rh.ResponseFailure): - if response.extra_info["type"] == "CommandError": - raise CommandError(response.extra_info["message"]) - raise CommandError(f"Herald action call failed:\n" - f"[p]{response}[/p]") - elif isinstance(response, rh.ResponseSuccess): - return response.data - else: - raise TypeError(f"Other Herald Link returned unknown response:\n" - f"[p]{response}[/p]") - - return GenericInterface - - def _data_factory(self) -> typing.Type[CommandData]: - raise NotImplementedError() - - def _init_network(self): - """Create a :py:class:`royalherald.Link`, and run it as a :py:class:`asyncio.Task`.""" - if self.uninitialized_network_config is not None: - self.network: rh.Link = rh.Link(self.uninitialized_network_config.master_uri, - self.uninitialized_network_config.master_secret, - self.interface_name, - self._network_handler) - log.debug(f"Running NetworkLink {self.network}") - self.loop.create_task(self.network.run()) - - async def _network_handler(self, message: typing.Union[rh.Request, rh.Broadcast]) -> rh.Response: - try: - network_handler = self.network_handlers[message.handler] - except KeyError: - log.warning(f"Missing network_handler for {message.handler}") - return rh.ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.") - else: - log.debug(f"Using {network_handler} as handler for {message.handler}") - if isinstance(message, rh.Request): - try: - response_data = await network_handler(self, **message.data) - return rh.ResponseSuccess(data=response_data) - except Exception as e: - sentry_sdk.capture_exception(e) - log.error(f"Exception {e} in {network_handler}") - return rh.ResponseFailure("exception_in_handler", - f"An exception was raised in {network_handler} for {message.handler}.", - extra_info={ - "type": e.__class__.__name__, - "message": str(e) - }) - elif isinstance(message, rh.Broadcast): - await network_handler(self, **message.data) - - def _init_database(self): - """Create an :py:class:`royalnet.database.Alchemy` with the tables required by the packs. Then, - find the chain that links the ``master_table`` to the ``identity_table``. """ - if self.uninitialized_database_config: - log.info(f"Alchemy: enabled") - required_tables = {self.uninitialized_database_config.master_table, self.uninitialized_database_config.identity_table} - for command in self.uninitialized_commands: - required_tables = required_tables.union(command.tables) - log.debug(f"Required tables: {', '.join([item.__qualname__ for item in required_tables])}") - self.alchemy = Alchemy(self.uninitialized_database_config.database_uri, required_tables) - self.master_table = self.alchemy.__getattribute__(self.uninitialized_database_config.master_table.__name__) - log.debug(f"Master table: {self.master_table.__qualname__}") - self.identity_table = self.alchemy.__getattribute__(self.uninitialized_database_config.identity_table.__name__) - log.debug(f"Identity table: {self.identity_table.__qualname__}") - self.identity_column = self.identity_table.__getattribute__(self.identity_table, - self.uninitialized_database_config.identity_column_name) - log.debug(f"Identity column: {self.identity_column.__class__.__qualname__}") - self.identity_chain = relationshiplinkchain(self.master_table, self.identity_table) - log.debug(f"Identity chain: {' -> '.join([str(item) for item in self.identity_chain])}") - else: - log.info(f"Alchemy: disabled") - self.alchemy = None - self.master_table = None - self.identity_table = None - self.identity_column = None - - def _init_sentry(self): - if self.uninitialized_sentry_dsn: - # noinspection PyUnreachableCode - if __debug__: - release = "DEV" - else: - release = royalnet.version.semantic - log.info(f"Sentry: enabled (Royalnet {release})") - self.sentry = sentry_sdk.init(self.uninitialized_sentry_dsn, - integrations=[AioHttpIntegration(), - SqlalchemyIntegration(), - LoggingIntegration(event_level=None)], - release=release) - else: - log.info("Sentry: disabled") - - def _init_loop(self): - if self.uninitialized_loop is None: - self.loop = asyncio.get_event_loop() - else: - self.loop = self.uninitialized_loop - - def __init__(self, *, - network_config: typing.Optional[rh.Config] = None, - database_config: typing.Optional[DatabaseConfig] = None, - commands: typing.List[typing.Type[Command]] = None, - sentry_dsn: typing.Optional[str] = None, - loop: asyncio.AbstractEventLoop = None, - secrets_name: str = "__default__"): - self.initialized = False - self.uninitialized_network_config = network_config - self.uninitialized_database_config = database_config - self.uninitialized_commands = commands - self.uninitialized_sentry_dsn = sentry_dsn - self.uninitialized_loop = loop - self.secrets_name = secrets_name - - def get_secret(self, username: str): - return keyring.get_password(f"Royalnet/{self.secrets_name}", username) - - def set_secret(self, username: str, password: str): - return keyring.set_password(f"Royalnet/{self.secrets_name}", username, password) - - def _initialize(self): - if not self.initialized: - self._init_sentry() - self._init_loop() - self._init_database() - self._init_commands() - self._init_network() - self.initialized = True - - def run(self): - """A blocking coroutine that should make the bot start listening to packs and requests.""" - raise NotImplementedError() - - def run_blocking(self, verbose=False): - if verbose: - core_logger = logging.root - core_logger.setLevel(logging.DEBUG) - stream_handler = logging.StreamHandler() - stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") - core_logger.addHandler(stream_handler) - core_logger.debug("Logging setup complete.") - self._initialize() - self.loop.run_until_complete(self.run()) diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py deleted file mode 100644 index d75a1d8b..00000000 --- a/royalnet/bots/telegram.py +++ /dev/null @@ -1,236 +0,0 @@ -import telegram -import telegram.utils.request -import uuid -import urllib3 -import asyncio -import sentry_sdk -import logging as _logging -import warnings -from .generic import GenericBot -from ..utils import * -from ..error import * -from ..commands import * - - -log = _logging.getLogger(__name__) - - -class TelegramBot(GenericBot): - """A bot that connects to `Telegram `_.""" - interface_name = "telegram" - - def _init_client(self): - """Create the :py:class:`telegram.Bot`, and set the starting offset.""" - # https://github.com/python-telegram-bot/python-telegram-bot/issues/341 - request = telegram.utils.request.Request(5, read_timeout=30) - token = self.get_secret("telegram") - self.client = telegram.Bot(token, request=request) - self._offset: int = -100 - - def _interface_factory(self) -> typing.Type[CommandInterface]: - # noinspection PyPep8Naming - GenericInterface = super()._interface_factory() - - # noinspection PyMethodParameters,PyAbstractClass - class TelegramInterface(GenericInterface): - name = self.interface_name - prefix = "/" - - def __init__(self): - super().__init__() - self.keys_callbacks: typing.Dict[typing.Tuple[int, str], typing.Callable] = {} - - def register_keyboard_key(interface, key_name: str, callback: typing.Callable): - interface.keys_callbacks[key_name] = callback - - def unregister_keyboard_key(interface, key_name: str): - try: - del interface.keys_callbacks[key_name] - except KeyError: - raise KeyError(f"Key '{key_name}' is not registered") - - return TelegramInterface - - def _data_factory(self) -> typing.Type[CommandData]: - # noinspection PyMethodParameters,PyAbstractClass - class TelegramData(CommandData): - def __init__(data, interface: CommandInterface, update: telegram.Update): - super().__init__(interface) - data.update = update - - async def reply(data, text: str): - await TelegramBot.safe_api_call(data.update.effective_chat.send_message, - telegram_escape(text), - parse_mode="HTML", - disable_web_page_preview=True) - - async def get_author(data, error_if_none=False): - if data.update.message is not None: - user: telegram.User = data.update.message.from_user - elif data.update.callback_query is not None: - user: telegram.user = data.update.callback_query.from_user - else: - raise CommandError("Command caller can not be determined") - if user is None: - if error_if_none: - raise CommandError("No command caller for this message") - return None - query = data.session.query(self.master_table) - for link in self.identity_chain: - query = query.join(link.mapper.class_) - query = query.filter(self.identity_column == user.id) - result = await asyncify(query.one_or_none) - if result is None and error_if_none: - raise CommandError("Command caller is not registered") - return result - - async def keyboard(data, text: str, keyboard: typing.Dict[str, typing.Callable]) -> None: - warnings.warn("keyboard is deprecated, please avoid using it", category=DeprecationWarning) - tg_keyboard = [] - for key in keyboard: - press_id = uuid.uuid4() - tg_keyboard.append([telegram.InlineKeyboardButton(key, callback_data=str(press_id))]) - data._interface.register_keyboard_key(key_name=str(press_id), callback=keyboard[key]) - await TelegramBot.safe_api_call(data.update.effective_chat.send_message, - telegram_escape(text), - reply_markup=telegram.InlineKeyboardMarkup(tg_keyboard), - parse_mode="HTML", - disable_web_page_preview=True) - - async def delete_invoking(data, error_if_unavailable=False) -> None: - message: telegram.Message = data.update.message - await TelegramBot.safe_api_call(message.delete) - - return TelegramData - - @staticmethod - async def safe_api_call(f: typing.Callable, *args, **kwargs) -> typing.Optional: - while True: - try: - return await asyncify(f, *args, **kwargs) - except telegram.error.TimedOut as error: - log.debug(f"Timed out during {f.__qualname__} (retrying immediatly): {error}") - continue - except telegram.error.NetworkError as error: - log.debug(f"Network error during {f.__qualname__} (skipping): {error}") - break - except telegram.error.Unauthorized as error: - log.info(f"Unauthorized to run {f.__qualname__} (skipping): {error}") - break - except telegram.error.RetryAfter as error: - log.warning(f"Rate limited during {f.__qualname__} (retrying in 15s): {error}") - await asyncio.sleep(15) - continue - except urllib3.exceptions.HTTPError as error: - log.warning(f"urllib3 HTTPError during {f.__qualname__} (retrying in 15s): {error}") - await asyncio.sleep(15) - continue - except Exception as error: - log.error(f"{error.__class__.__qualname__} during {f} (skipping): {error}") - sentry_sdk.capture_exception(error) - break - return None - - async def _handle_update(self, update: telegram.Update): - # Skip non-message updates - if update.message is not None: - await self._handle_message(update) - elif update.callback_query is not None: - await self._handle_callback_query(update) - - async def _handle_message(self, update: telegram.Update): - message: telegram.Message = update.message - text: str = message.text - # Try getting the caption instead - if text is None: - text: str = message.caption - # No text or caption, ignore the message - if text is None: - return - # Skip non-command updates - if not text.startswith("/"): - return - # Find and clean parameters - command_text, *parameters = text.split(" ") - command_name = command_text.replace(f"@{self.client.username}", "").lower() - # Send a typing notification - await self.safe_api_call(update.message.chat.send_action, telegram.ChatAction.TYPING) - # Find the command - try: - command = self.commands[command_name] - except KeyError: - # Skip the message - return - # Prepare data - data = self._Data(interface=command.interface, update=update) - try: - # Run the command - await command.run(CommandArgs(parameters), data) - except InvalidInputError as e: - await data.reply(f"âš ï¸ {e.message}\n" - f"Syntax: [c]/{command.name} {command.syntax}[/c]") - except UnsupportedError as e: - await data.reply(f"âš ï¸ {e.message}") - except CommandError as e: - await data.reply(f"âš ï¸ {e.message}") - except Exception as e: - sentry_sdk.capture_exception(e) - error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" - error_message += '\n'.join(e.args) - await data.reply(error_message) - finally: - # Close the data session - await data.session_close() - - async def _handle_callback_query(self, update: telegram.Update): - query: telegram.CallbackQuery = update.callback_query - source: telegram.Message = query.message - callback: typing.Optional[typing.Callable] = None - command: typing.Optional[Command] = None - for command in self.commands.values(): - if query.data in command.interface.keys_callbacks: - callback = command.interface.keys_callbacks[query.data] - break - if callback is None: - await self.safe_api_call(source.edit_reply_markup, reply_markup=None) - await self.safe_api_call(query.answer, text="â›”ï¸ This keyboard has expired.") - return - try: - response = await callback(data=self._Data(interface=command.interface, update=update)) - except KeyboardExpiredError as e: - # FIXME: May cause a memory leak, as keys are not deleted after use - await self.safe_api_call(source.edit_reply_markup, reply_markup=None) - if len(e.args) > 0: - await self.safe_api_call(query.answer, text=f"â›”ï¸ {e.args[0]}") - else: - await self.safe_api_call(query.answer, text="â›”ï¸ This keyboard has expired.") - return - except Exception as e: - error_text = f"â›”ï¸ {e.__class__.__name__}\n" - error_text += '\n'.join(e.args) - await self.safe_api_call(query.answer, text=error_text) - else: - await self.safe_api_call(query.answer, text=response) - - def _initialize(self): - super()._initialize() - self._init_client() - - async def run(self): - if not self.initialized: - self._initialize() - while True: - # Get the latest 100 updates - last_updates: typing.List[telegram.Update] = await self.safe_api_call(self.client.get_updates, - offset=self._offset, - timeout=30, - read_latency=5.0) - # Handle updates - for update in last_updates: - # noinspection PyAsyncCall - self.loop.create_task(self._handle_update(update)) - # Recalculate offset - try: - self._offset = last_updates[-1].update_id + 1 - except IndexError: - pass diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index b4fc1cc3..eaef11cd 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -2,7 +2,13 @@ from .commandinterface import CommandInterface from .command import Command from .commanddata import CommandData from .commandargs import CommandArgs -from .commanderrors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError +from .event import Event +from .errors import CommandError, \ + InvalidInputError, \ + UnsupportedError, \ + ConfigurationError, \ + ExternalError, \ + UserError __all__ = [ "CommandInterface", @@ -12,6 +18,8 @@ __all__ = [ "CommandError", "InvalidInputError", "UnsupportedError", - "KeyboardExpiredError", "ConfigurationError", + "ExternalError", + "UserError", + "Event" ] diff --git a/royalnet/commands/__pycache__/__init__.cpython-37.pyc b/royalnet/commands/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 00000000..b335d811 Binary files /dev/null and b/royalnet/commands/__pycache__/__init__.cpython-37.pyc differ diff --git a/royalnet/commands/__pycache__/command.cpython-37.pyc b/royalnet/commands/__pycache__/command.cpython-37.pyc new file mode 100644 index 00000000..0a7b7243 Binary files /dev/null and b/royalnet/commands/__pycache__/command.cpython-37.pyc differ diff --git a/royalnet/commands/__pycache__/commandargs.cpython-37.pyc b/royalnet/commands/__pycache__/commandargs.cpython-37.pyc new file mode 100644 index 00000000..f131bab6 Binary files /dev/null and b/royalnet/commands/__pycache__/commandargs.cpython-37.pyc differ diff --git a/royalnet/commands/__pycache__/commanddata.cpython-37.pyc b/royalnet/commands/__pycache__/commanddata.cpython-37.pyc new file mode 100644 index 00000000..a9bd4217 Binary files /dev/null and b/royalnet/commands/__pycache__/commanddata.cpython-37.pyc differ diff --git a/royalnet/commands/__pycache__/commanderrors.cpython-37.pyc b/royalnet/commands/__pycache__/commanderrors.cpython-37.pyc new file mode 100644 index 00000000..aaa01f10 Binary files /dev/null and b/royalnet/commands/__pycache__/commanderrors.cpython-37.pyc differ diff --git a/royalnet/commands/__pycache__/commandinterface.cpython-37.pyc b/royalnet/commands/__pycache__/commandinterface.cpython-37.pyc new file mode 100644 index 00000000..209b8510 Binary files /dev/null and b/royalnet/commands/__pycache__/commandinterface.cpython-37.pyc differ diff --git a/royalnet/commands/command.py b/royalnet/commands/command.py index 3e22ae45..67d4098e 100644 --- a/royalnet/commands/command.py +++ b/royalnet/commands/command.py @@ -7,33 +7,34 @@ from .commanddata import CommandData class Command: name: str = NotImplemented """The main name of the command. - To have ``/example`` on Telegram, the name should be ``example``.""" + + Example: + To be able to call ``/example`` on Telegram, the name should be ``"example"``.""" aliases: typing.List[str] = [] """A list of possible aliases for a command. - To have ``/e`` as alias for ``/example``, one should set aliases to ``["e"]``.""" + + Example: + To be able to call ``/e`` as an alias for ``/example``, one should set aliases to ``["e"]``.""" description: str = NotImplemented """A small description of the command, to be displayed when the command is being autocompleted.""" syntax: str = "" - """The syntax of the command, to be displayed when a :py:exc:`royalnet.error.InvalidInputError` is raised, + """The syntax of the command, to be displayed when a :py:exc:`InvalidInputError` is raised, in the format ``(required_arg) [optional_arg]``.""" - tables: typing.Set = set() - """A set of :py:class:`royalnet.database` tables that must exist for this command to work.""" - def __init__(self, interface: CommandInterface): self.interface = interface @property def alchemy(self): - """A shortcut to ``self.interface.alchemy``.""" + """A shortcut for :attr:`.interface.alchemy`.""" return self.interface.alchemy @property def loop(self): - """A shortcut to ``self.interface.loop``.""" + """A shortcut for :attr:`.interface.loop`.""" return self.interface.loop async def run(self, args: CommandArgs, data: CommandData) -> None: diff --git a/royalnet/commands/commandargs.py b/royalnet/commands/commandargs.py index 34870ae5..5781c4bb 100644 --- a/royalnet/commands/commandargs.py +++ b/royalnet/commands/commandargs.py @@ -1,16 +1,33 @@ import re -import typing -from .commanderrors import InvalidInputError +from typing import Pattern, AnyStr, Optional, Sequence, Union +from .errors import InvalidInputError class CommandArgs(list): - """An interface to access the arguments of a command with ease.""" + """An interface to easily access the arguments of a command. + + Inherits from :class:`list`.""" def __getitem__(self, item): - """Arguments can be accessed with an array notation, such as ``args[0]``. + """Access arguments as if they were a :class:`list`. Raises: - royalnet.error.InvalidInputError: if the requested argument does not exist.""" + InvalidInputError: if the requested argument does not exist. + + Examples: + :: + + # /pasta spaghetti aldente + >>> self[0] + "spaghetti" + >>> self[1] + "aldente" + >>> self[2] + # InvalidInputError: Missing argument #3. + >>> self[0:2] + ["spaghetti", "aldente"] + + """ if isinstance(item, int): try: return super().__getitem__(item) @@ -27,43 +44,68 @@ class CommandArgs(list): """Get the arguments as a space-joined string. Parameters: - require_at_least: the minimum amount of arguments required, will raise :py:exc:`royalnet.error.InvalidInputError` if the requirement is not fullfilled. + require_at_least: the minimum amount of arguments required. Raises: - royalnet.error.InvalidInputError: if there are less than ``require_at_least`` arguments. + InvalidInputError: if there are less than ``require_at_least`` arguments. Returns: - The space-joined string.""" + The space-joined string. + + Examples: + :: + + # /pasta spaghetti aldente + >>> self.joined() + "spaghetti aldente" + >>> self.joined(require_at_least=3) + # InvalidInputError: Not enough arguments specified (minimum is 3). + + """ if len(self) < require_at_least: raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).") return " ".join(self) - def match(self, pattern: typing.Union[str, typing.Pattern], *flags) -> typing.Sequence[typing.AnyStr]: - """Match the :py:func:`royalnet.utils.commandargs.joined` to a regex pattern. + def match(self, pattern: Union[str, Pattern], *flags) -> Sequence[AnyStr]: + """Match the :meth:`.joined` string to a :class:`re.Pattern`-like object. Parameters: - pattern: The regex pattern to be passed to :py:func:`re.match`. + pattern: The regex pattern to be passed to :func:`re.match`. Raises: - royalnet.error.InvalidInputError: if the pattern doesn't match. + InvalidInputError: if the pattern doesn't match. Returns: - The matched groups, as returned by :py:func:`re.Match.groups`.""" + The matched groups, as returned by :func:`re.Match.groups`.""" text = self.joined() match = re.match(pattern, text, *flags) if match is None: raise InvalidInputError("Invalid syntax.") return match.groups() - def optional(self, index: int, default=None) -> typing.Optional[str]: - """Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the ``default`` value. + def optional(self, index: int, default=None) -> Optional[str]: + """Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the + ``default`` value. Parameters: index: The index of the argument you want to retrieve. default: The value returned if the argument is missing. Returns: - Either the argument or the ``default`` value, defaulting to ``None``.""" + Either the argument or the ``default`` value, defaulting to ``None``. + + Examples: + :: + + # /pasta spaghetti aldente + >>> self.optional(0) + "spaghetti" + >>> self.optional(2) + None + >>> self.optional(2, default="carbonara") + "carbonara" + + """ try: return self[index] except InvalidInputError: diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 2f98006b..a5d4d18d 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -1,36 +1,37 @@ -import typing -import warnings -from .commanderrors import UnsupportedError +from asyncio import AbstractEventLoop +from typing import Optional, TYPE_CHECKING +from .errors import UnsupportedError from .commandinterface import CommandInterface from ..utils import asyncify +from sqlalchemy.orm.session import Session class CommandData: - def __init__(self, interface: CommandInterface): + def __init__(self, interface: CommandInterface, session: Optional[Session], loop: AbstractEventLoop): self._interface: CommandInterface = interface - if len(self._interface.command.tables) > 0: - self.session = self._interface.alchemy.Session() - else: - self.session = None + self._session: Optional[Session] = session + self.loop: AbstractEventLoop = loop + + @property + def session(self) -> Session: + """Get the :class:`~royalnet.alchemy.Alchemy` :class:`Session`, if it is available. + + Raises: + UnsupportedError: if no session is available.""" + if self._session is None: + raise UnsupportedError("'session' is not supported") + return self._session async def session_commit(self): """Commit the changes to the session.""" await asyncify(self.session.commit) - async def session_close(self): - """Close the opened session. - - Remember to call this when the data is disposed of!""" - if self.session: - await asyncify(self.session.close) - self.session = None - async def reply(self, text: str) -> None: """Send a text message to the channel where the call was made. Parameters: text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using.""" - raise UnsupportedError("'reply' is not supported on this platform") + raise UnsupportedError("'reply' is not supported") async def get_author(self, error_if_none: bool = False): """Try to find the identifier of the user that sent the message. @@ -38,14 +39,7 @@ class CommandData: Parameters: error_if_none: Raise an exception if this is True and the call has no author.""" - raise UnsupportedError("'get_author' is not supported on this platform") - - async def keyboard(self, text: str, keyboard: typing.Dict[str, typing.Callable]) -> None: - """Send a keyboard having the keys of the dict as keys and calling the correspondent values on a press. - - The function should be passed the :py:class:`CommandData` instance as a argument.""" - warnings.warn("keyboard is deprecated, please avoid using it", category=DeprecationWarning) - raise UnsupportedError("'keyboard' is not supported on this platform") + raise UnsupportedError("'get_author' is not supported") async def delete_invoking(self, error_if_unavailable=False) -> None: """Delete the invoking message, if supported by the interface. @@ -55,4 +49,4 @@ class CommandData: Parameters: error_if_unavailable: if True, raise an exception if the message cannot been deleted.""" if error_if_unavailable: - raise UnsupportedError("'delete_invoking' is not supported on this platform") + raise UnsupportedError("'delete_invoking' is not supported") diff --git a/royalnet/commands/commanderrors.py b/royalnet/commands/commanderrors.py deleted file mode 100644 index fbd5725b..00000000 --- a/royalnet/commands/commanderrors.py +++ /dev/null @@ -1,29 +0,0 @@ -class CommandError(Exception): - """Something went wrong during the execution of this command. - - Display an error message to the user, explaining what went wrong.""" - def __init__(self, message=""): - self.message = message - - def __repr__(self): - return f"{self.__class__.__qualname__}({repr(self.message)})" - - -class InvalidInputError(CommandError): - """The command has received invalid input and cannot complete. - - Display an error message to the user, along with the correct syntax for the command.""" - - -class UnsupportedError(CommandError): - """A requested feature is not available on this interface. - - Display an error message to the user, telling them to use another interface.""" - - -class KeyboardExpiredError(CommandError): - """A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.""" - - -class ConfigurationError(CommandError): - """The command is misconfigured and cannot work.""" diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py index 5050d77a..d8c18db3 100644 --- a/royalnet/commands/commandinterface.py +++ b/royalnet/commands/commandinterface.py @@ -1,35 +1,63 @@ -import typing -import asyncio -from .commanderrors import UnsupportedError -if typing.TYPE_CHECKING: +from typing import * +from asyncio import AbstractEventLoop +from .errors import UnsupportedError +if TYPE_CHECKING: + from .event import Event from .command import Command - from ..database import Alchemy - from ..bots import GenericBot + from ..alchemy import Alchemy + from ..serf import Serf + from ..constellation import Constellation class CommandInterface: name: str = NotImplemented + """The name of the :class:`CommandInterface` that's being implemented. + + Examples: + ``telegram``, ``discord``, ``console``...""" + prefix: str = NotImplemented - alchemy: "Alchemy" = NotImplemented - bot: "GenericBot" = NotImplemented - loop: asyncio.AbstractEventLoop = NotImplemented + """The prefix used by commands on the interface. + + Examples: + ``/`` on Telegram, ``!`` on Discord.""" - def __init__(self): - self.command: typing.Optional[Command] = None # Will be bound after the command has been created + serf: Optional["Serf"] = None + """A reference to the :class:`~royalnet.serf.Serf` that is implementing this :class:`CommandInterface`. + + Example: + A reference to a :class:`~royalnet.serf.telegram.TelegramSerf`.""" - def register_herald_action(self, - event_name: str, - coroutine: typing.Callable[[typing.Any], typing.Awaitable[typing.Dict]]): - raise UnsupportedError(f"{self.register_herald_action.__name__} is not supported on this platform") + constellation: Optional["Constellation"] = None + """A reference to the Constellation that is implementing this :class:`CommandInterface`. + + Example: + A reference to a :class:`~royalnet.constellation.Constellation`.""" - def unregister_herald_action(self, event_name: str): - raise UnsupportedError(f"{self.unregister_herald_action.__name__} is not supported on this platform") - async def call_herald_action(self, destination: str, event_name: str, args: typing.Dict) -> typing.Dict: - raise UnsupportedError(f"{self.call_herald_action.__name__} is not supported on this platform") + @property + def alchemy(self) -> "Alchemy": + """A shortcut for :attr:`.serf.alchemy`.""" + return self.serf.alchemy - def register_keyboard_key(self, key_name: str, callback: typing.Callable): - raise UnsupportedError(f"{self.register_keyboard_key.__name__} is not supported on this platform") + @property + def loop(self) -> AbstractEventLoop: + """A shortcut for :attr:`.serf.loop`.""" + return self.serf.loop - def unregister_keyboard_key(self, key_name: str): - raise UnsupportedError(f"{self.unregister_keyboard_key.__name__} is not supported on this platform") + def __init__(self, cfg: Dict[str, Any]): + self.cfg: Dict[str, Any] = cfg + """The config section for the pack of the command.""" + + # Will be bound after the command/event has been created + self.command: Optional[Command] = None + self.event: Optional[Event] = None + + async def call_herald_event(self, destination: str, event_name: str, **kwargs) -> dict: + """Call an event function on a different :class:`~royalnet.serf.Serf`. + + Example: + You can run a function on a :class:`~royalnet.serf.discord.DiscordSerf` from a + :class:`~royalnet.serf.telegram.TelegramSerf`. + """ + raise UnsupportedError(f"{self.call_herald_event.__name__} is not supported on this platform") diff --git a/royalnet/commands/errors.py b/royalnet/commands/errors.py new file mode 100644 index 00000000..45baa5b3 --- /dev/null +++ b/royalnet/commands/errors.py @@ -0,0 +1,30 @@ +class CommandError(Exception): + """Something went wrong during the execution of this command. + + Display an error message to the user, explaining what went wrong.""" + def __init__(self, message=""): + self.message = message + + def __repr__(self): + return f"{self.__class__.__qualname__}({repr(self.message)})" + + +class UserError(CommandError): + """The command failed to execute, and the error is because of something that the user did.""" + + +class InvalidInputError(UserError): + """The command has received invalid input and cannot complete.""" + + +class UnsupportedError(CommandError): + """A requested feature is not available on this interface.""" + + +class ConfigurationError(CommandError): + """The command cannot work because of a wrong configuration by part of the Royalnet admin.""" + + +class ExternalError(CommandError): + """The command failed to execute, but the problem was because of an external factor (such as an external API going + down).""" diff --git a/royalnet/commands/event.py b/royalnet/commands/event.py new file mode 100644 index 00000000..af77a1ae --- /dev/null +++ b/royalnet/commands/event.py @@ -0,0 +1,29 @@ +from .commandinterface import CommandInterface +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from serf import Serf + + +class Event: + """A remote procedure call triggered by a :mod:`royalnet.herald` request.""" + + name = NotImplemented + """The event_name that will trigger this event.""" + + def __init__(self, interface: CommandInterface): + """Bind the event to a :class:`~royalnet.serf.Serf`.""" + self.interface: CommandInterface = interface + """The :class:`CommandInterface` available to this :class:`Event`.""" + + @property + def alchemy(self): + """A shortcut for :attr:`.interface.serf.alchemy`.""" + return self.interface.serf.alchemy + + @property + def loop(self): + """A shortcut for :attr:`.interface.serf.loop`""" + return self.interface.serf.loop + + async def run(self, **kwargs): + raise NotImplementedError() diff --git a/royalnet/configurator.py b/royalnet/configurator.py deleted file mode 100644 index fc78e573..00000000 --- a/royalnet/configurator.py +++ /dev/null @@ -1,30 +0,0 @@ -import click -import keyring - - -@click.command() -def run(): - click.echo("Welcome to the Royalnet configuration creator!") - secrets_name = click.prompt("Desired secrets name", default="__default__") - network = click.prompt("Network password", default="") - if network: - keyring.set_password(f"Royalnet/{secrets_name}", "network", network) - telegram = click.prompt("Telegram Bot API token", default="") - if telegram: - keyring.set_password(f"Royalnet/{secrets_name}", "telegram", telegram) - discord = click.prompt("Discord Bot API token", default="") - if discord: - keyring.set_password(f"Royalnet/{secrets_name}", "discord", discord) - imgur = click.prompt("Imgur API token", default="") - if imgur: - keyring.set_password(f"Royalnet/{secrets_name}", "imgur", imgur) - sentry = click.prompt("Sentry DSN", default="") - if sentry: - keyring.set_password(f"Royalnet/{secrets_name}", "sentry", sentry) - leagueoflegends = click.prompt("League of Legends API Token", default="") - if leagueoflegends: - keyring.set_password(f"Royalnet/{secrets_name}", "leagueoflegends", leagueoflegends) - - -if __name__ == "__main__": - run() diff --git a/royalnet/constellation/README.md b/royalnet/constellation/README.md new file mode 100644 index 00000000..c14c3480 --- /dev/null +++ b/royalnet/constellation/README.md @@ -0,0 +1,11 @@ +# `royalnet.constellation` + +The part of `royalnet` that handles the webserver and webpages. + +It uses many features of [`starlette`](https://www.starlette.io). + +## Hierarchy + +- `constellation` + - `star` + - `shoot` \ No newline at end of file diff --git a/royalnet/web/__init__.py b/royalnet/constellation/__init__.py similarity index 52% rename from royalnet/web/__init__.py rename to royalnet/constellation/__init__.py index f1fd3545..effea7b3 100644 --- a/royalnet/web/__init__.py +++ b/royalnet/constellation/__init__.py @@ -1,11 +1,15 @@ +"""The part of :mod:`royalnet` that handles the webserver and webpages. + +It uses many features of :mod:`starlette`.""" + from .constellation import Constellation from .star import Star, PageStar, ExceptionStar -from .error import error +from .shoot import shoot __all__ = [ "Constellation", "Star", "PageStar", "ExceptionStar", - "error", + "shoot", ] diff --git a/royalnet/constellation/__pycache__/__init__.cpython-37.pyc b/royalnet/constellation/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 00000000..2d839f68 Binary files /dev/null and b/royalnet/constellation/__pycache__/__init__.cpython-37.pyc differ diff --git a/royalnet/constellation/__pycache__/constellation.cpython-37.pyc b/royalnet/constellation/__pycache__/constellation.cpython-37.pyc new file mode 100644 index 00000000..4122eaf6 Binary files /dev/null and b/royalnet/constellation/__pycache__/constellation.cpython-37.pyc differ diff --git a/royalnet/constellation/__pycache__/star.cpython-37.pyc b/royalnet/constellation/__pycache__/star.cpython-37.pyc new file mode 100644 index 00000000..17faaf03 Binary files /dev/null and b/royalnet/constellation/__pycache__/star.cpython-37.pyc differ diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py new file mode 100644 index 00000000..809939dc --- /dev/null +++ b/royalnet/constellation/constellation.py @@ -0,0 +1,309 @@ +import logging +import importlib +import asyncio as aio +from typing import * +import royalnet.alchemy as ra +import royalnet.herald as rh +import royalnet.utils as ru +import royalnet.commands as rc +from .star import PageStar, ExceptionStar + +try: + import uvicorn + from starlette.applications import Starlette +except ImportError: + uvicorn = None + Starlette = None + +try: + import sentry_sdk +except ImportError: + sentry_sdk = None + AioHttpIntegration = None + SqlalchemyIntegration = None + LoggingIntegration = None + +try: + import coloredlogs +except ImportError: + coloredlogs = None + + +log = logging.getLogger(__name__) + +UVICORN_LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": True, + "formatters": {}, + "handlers": {}, + "loggers": {}, +} + + +class Constellation: + """The class that represents the webserver. + + It runs multiple :class:`Star`, which represent the routes of the website. + + It also handles the :class:`Alchemy` connection, and Herald connections too.""" + def __init__(self, + alchemy_cfg: Dict[str, Any], + herald_cfg: Dict[str, Any], + packs_cfg: Dict[str, Any], + constellation_cfg: Dict[str, Any], + **_): + if Starlette is None: + raise ImportError("`constellation` extra is not installed") + + # Import packs + pack_names = packs_cfg["active"] + packs = {} + for pack_name in pack_names: + log.debug(f"Importing pack: {pack_name}") + try: + packs[pack_name] = importlib.import_module(pack_name) + except ImportError as e: + log.error(f"Error during the import of {pack_name}: {e}") + log.info(f"Packs: {len(packs)} imported") + + self.alchemy = None + """The :class:`~ra.Alchemy` of this Constellation.""" + + # Alchemy + if ra.Alchemy is None: + log.info("Alchemy: not installed") + elif not alchemy_cfg["enabled"]: + log.info("Alchemy: disabled") + else: + # Find all tables + tables = set() + for pack in packs.values(): + try: + tables = tables.union(pack.available_tables) + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.") + continue + # Create the Alchemy + self.alchemy = ra.Alchemy(alchemy_cfg["database_url"], tables) + log.info(f"Alchemy: {self.alchemy}") + + # Herald + self.herald: Optional[rh.Link] = None + """The :class:`Link` object connecting the :class:`Constellation` to the rest of the herald network.""" + + self.herald_task: Optional[aio.Task] = None + """A reference to the :class:`aio.Task` that runs the :class:`rh.Link`.""" + + self.Interface: Type[rc.CommandInterface] = self.interface_factory() + """The :class:`~rc.CommandInterface` class of this :class:`Constellation`.""" + + self.events: Dict[str, rc.Event] = {} + """A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`.""" + + self.starlette = Starlette(debug=__debug__) + """The :class:`~starlette.Starlette` app.""" + + # Register Events + for pack_name in packs: + pack = packs[pack_name] + pack_cfg = packs_cfg.get(pack_name, {}) + try: + events = pack.available_events + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_events` attribute.") + else: + self.register_events(events, pack_cfg) + log.info(f"Events: {len(self.events)} events") + + if rh.Link is None: + log.info("Herald: not installed") + elif not herald_cfg["enabled"]: + log.info("Herald: disabled") + else: + self.init_herald(herald_cfg) + log.info(f"Herald: enabled") + + # Register PageStars and ExceptionStars + for pack_name in packs: + pack = packs[pack_name] + pack_cfg = packs_cfg.get(pack_name, {}) + try: + page_stars = pack.available_page_stars + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_page_stars` attribute.") + else: + self.register_page_stars(page_stars, pack_cfg) + try: + exc_stars = pack.available_exception_stars + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_exception_stars` attribute.") + else: + self.register_exc_stars(exc_stars, pack_cfg) + log.info(f"PageStars: {len(self.starlette.routes)} stars") + log.info(f"ExceptionStars: {len(self.starlette.exception_handlers)} stars") + + self.running: bool = False + """Is the :class:`Constellation` server currently running?""" + + self.address: str = constellation_cfg["address"] + """The address that the :class:`Constellation` will bind to when run.""" + + self.port: int = constellation_cfg["port"] + """The port on which the :class:`Constellation` will listen for connection on.""" + + # TODO: is this a good idea? + def interface_factory(self) -> Type[rc.CommandInterface]: + """Create the :class:`rc.CommandInterface` class for the :class:`Constellation`.""" + + # noinspection PyMethodParameters + class GenericInterface(rc.CommandInterface): + alchemy: ra.Alchemy = self.alchemy + constellation = self + + async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict: + """Send a :class:`rh.Request` to a specific destination, and wait for a + :class:`rh.Response`.""" + if self.herald is None: + raise rc.UnsupportedError("`royalherald` is not enabled on this Constellation.") + request: rh.Request = rh.Request(handler=event_name, data=kwargs) + response: rh.Response = await self.herald.request(destination=destination, request=request) + if isinstance(response, rh.ResponseFailure): + if response.name == "no_event": + raise rc.CommandError(f"There is no event named {event_name} in {destination}.") + elif response.name == "exception_in_event": + # TODO: pretty sure there's a better way to do this + if response.extra_info["type"] == "CommandError": + raise rc.CommandError(response.extra_info["message"]) + elif response.extra_info["type"] == "UserError": + raise rc.UserError(response.extra_info["message"]) + elif response.extra_info["type"] == "InvalidInputError": + raise rc.InvalidInputError(response.extra_info["message"]) + elif response.extra_info["type"] == "UnsupportedError": + raise rc.UnsupportedError(response.extra_info["message"]) + elif response.extra_info["type"] == "ConfigurationError": + raise rc.ConfigurationError(response.extra_info["message"]) + elif response.extra_info["type"] == "ExternalError": + raise rc.ExternalError(response.extra_info["message"]) + else: + raise TypeError(f"Herald action call returned invalid error:\n" + f"[p]{response}[/p]") + elif isinstance(response, rh.ResponseSuccess): + return response.data + else: + raise TypeError(f"Other Herald Link returned unknown response:\n" + f"[p]{response}[/p]") + + return GenericInterface + + def init_herald(self, herald_cfg: Dict[str, Any]): + """Create a :class:`rh.Link`.""" + herald_cfg["name"] = "constellation" + self.herald: rh.Link = rh.Link(rh.Config.from_config(**herald_cfg), self.network_handler) + + async def network_handler(self, message: Union[rh.Request, rh.Broadcast]) -> rh.Response: + try: + event: rc.Event = self.events[message.handler] + except KeyError: + log.warning(f"No event for '{message.handler}'") + return rh.ResponseFailure("no_event", f"This serf does not have any event for {message.handler}.") + log.debug(f"Event called: {event.name}") + if isinstance(message, rh.Request): + try: + response_data = await event.run(**message.data) + return rh.ResponseSuccess(data=response_data) + except Exception as e: + ru.sentry_exc(e) + return rh.ResponseFailure("exception_in_event", + f"An exception was raised in the event for '{message.handler}'.", + extra_info={ + "type": e.__class__.__qualname__, + "message": str(e) + }) + elif isinstance(message, rh.Broadcast): + await event.run(**message.data) + + def register_events(self, events: List[Type[rc.Event]], pack_cfg: Dict[str, Any]): + for SelectedEvent in events: + # Create a new interface + interface = self.Interface(cfg=pack_cfg) + # Initialize the event + try: + event = SelectedEvent(interface) + except Exception as e: + log.error(f"Skipping: " + f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.") + ru.sentry_exc(e) + continue + # Register the event + if SelectedEvent.name in self.events: + log.warning(f"Overriding (already defined): {SelectedEvent.__qualname__} -> {SelectedEvent.name}") + else: + log.debug(f"Registering: {SelectedEvent.__qualname__} -> {SelectedEvent.name}") + self.events[SelectedEvent.name] = event + + def register_page_stars(self, page_stars: List[Type[PageStar]], pack_cfg: Dict[str, Any]): + for SelectedPageStar in page_stars: + log.debug(f"Registering: {SelectedPageStar.path} -> {SelectedPageStar.__qualname__}") + try: + page_star_instance = SelectedPageStar(constellation=self, config=pack_cfg) + except Exception as e: + log.error(f"Skipping: " + f"{SelectedPageStar.__qualname__} - {e.__class__.__qualname__} in the initialization.") + ru.sentry_exc(e) + continue + self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods) + + def register_exc_stars(self, exc_stars: List[Type[ExceptionStar]], pack_cfg: Dict[str, Any]): + for SelectedPageStar in exc_stars: + log.debug(f"Registering: {SelectedPageStar.error} -> {SelectedPageStar.__qualname__}") + try: + page_star_instance = SelectedPageStar(constellation=self, config=pack_cfg) + except Exception as e: + log.error(f"Skipping: " + f"{SelectedPageStar.__qualname__} - {e.__class__.__qualname__} in the initialization.") + ru.sentry_exc(e) + continue + self.starlette.add_exception_handler(page_star_instance.error, page_star_instance.page) + + def run_blocking(self): + log.info(f"Running Constellation on https://{self.address}:{self.port}/...") + loop: aio.AbstractEventLoop = aio.get_event_loop() + self.running = True + # FIXME: might not work as expected + loop.create_task(self.herald.run()) + try: + uvicorn.run(self.starlette, host=self.address, port=self.port, log_config=UVICORN_LOGGING_CONFIG) + finally: + self.running = False + + @classmethod + def run_process(cls, + alchemy_cfg: Dict[str, Any], + herald_cfg: Dict[str, Any], + sentry_cfg: Dict[str, Any], + packs_cfg: Dict[str, Any], + constellation_cfg: Dict[str, Any], + logging_cfg: Dict[str, Any]): + """Blockingly create and run the Constellation. + + This should be used as the target of a :class:`multiprocessing.Process`.""" + ru.init_logging(logging_cfg) + + if sentry_cfg is None or not sentry_cfg["enabled"]: + log.info("Sentry: disabled") + else: + try: + ru.init_sentry(sentry_cfg) + except ImportError: + log.info("Sentry: not installed") + + constellation = cls(alchemy_cfg=alchemy_cfg, + herald_cfg=herald_cfg, + packs_cfg=packs_cfg, + constellation_cfg=constellation_cfg) + + # Run the server + constellation.run_blocking() + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {'running' if self.running else 'inactive'}>" diff --git a/royalnet/constellation/shoot.py b/royalnet/constellation/shoot.py new file mode 100644 index 00000000..f5c8471e --- /dev/null +++ b/royalnet/constellation/shoot.py @@ -0,0 +1,13 @@ +try: + from starlette.responses import JSONResponse +except ImportError: + JSONResponse = None + + +def shoot(code: int, description: str) -> JSONResponse: + """Create a error :class:`~starlette.response.JSONResponse` with the passed error code and description.""" + if JSONResponse is None: + raise ImportError("'constellation' extra is not installed") + return JSONResponse({ + "error": description + }, status_code=code) diff --git a/royalnet/constellation/star.py b/royalnet/constellation/star.py new file mode 100644 index 00000000..6824e57f --- /dev/null +++ b/royalnet/constellation/star.py @@ -0,0 +1,100 @@ +from typing import * +from starlette.requests import Request +from starlette.responses import Response + +if TYPE_CHECKING: + from .constellation import Constellation + + +class Star: + """A Star is a class representing a part of the website. + + It shouldn't be used directly: please use :class:`PageStar` and :class:`ExceptionStar` instead!""" + def __init__(self, config: Dict[str, Any], constellation: "Constellation"): + self.config: Dict[str, Any] = config + self.constellation: "Constellation" = constellation + + async def page(self, request: Request) -> Response: + """The function generating the :class:`~starlette.Response` to a web :class:`~starlette.Request`. + + If it raises an error, the corresponding :class:`ExceptionStar` will be used to handle the request instead.""" + raise NotImplementedError() + + @property + def alchemy(self): + """A shortcut for the :class:`~royalnet.alchemy.Alchemy` of the :class:`Constellation`.""" + return self.constellation.alchemy + + # noinspection PyPep8Naming + @property + def Session(self): + """A shortcut for the :class:`~royalnet.alchemy.Alchemy` :class:`Session` of the :class:`Constellation`.""" + return self.constellation.alchemy.Session + + @property + def session_acm(self): + """A shortcut for :func:`.alchemy.session_acm` of the :class:`Constellation`.""" + return self.constellation.alchemy.session_acm + + def __repr__(self): + return f"<{self.__class__.__qualname__}>" + + +class PageStar(Star): + """A PageStar is a class representing a single route of the website (for example, ``/api/user/get``). + + To create a new website route you should create a new class inheriting from this class with a function overriding + :meth:`.page` and changing the values of :attr:`.path` and optionally :attr:`.methods`.""" + path: str = NotImplemented + """The route of the star. + + Example: + :: + + path: str = '/api/user/get' + + """ + + methods: List[str] = ["GET"] + """The HTTP methods supported by the Star, in form of a list. + + By default, a Star only supports the ``GET`` method, but more can be added. + + Example: + :: + + methods: List[str] = ["GET", "POST", "PUT", "DELETE"] + + """ + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {self.path}>" + + +class ExceptionStar(Star): + """An ExceptionStar is a class that handles an :class:`Exception` raised by another star by returning a different + response than the one originally intended. + + The handled exception type is specified in the :attr:`.error`. + + It can also handle standard webserver errors, such as ``404 Not Found``: + to handle them, set :attr:`.error` to an :class:`int` of the corresponding error code. + + To create a new exception handler you should create a new class inheriting from this class with a function + overriding :meth:`.page` and changing the value of :attr:`.error`.""" + error: Union[Type[Exception], int] + """The error that should be handled by this star. It should be either a subclass of :exc:`Exception`, + or the :class:`int` of an HTTP error code. + + Examples: + :: + + error: int = 404 + + :: + + error: Type[Exception] = ValueError + """ + + def __repr__(self): + return f"<{self.__class__.__qualname__}: handles {self.error}>" diff --git a/royalnet/database/__init__.py b/royalnet/database/__init__.py deleted file mode 100644 index 9434c897..00000000 --- a/royalnet/database/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Relational database classes and methods.""" - -from .alchemy import Alchemy -from .relationshiplinkchain import relationshiplinkchain -from .databaseconfig import DatabaseConfig - -__all__ = ["Alchemy", "relationshiplinkchain", "DatabaseConfig"] diff --git a/royalnet/database/alchemy.py b/royalnet/database/alchemy.py deleted file mode 100644 index 2f164368..00000000 --- a/royalnet/database/alchemy.py +++ /dev/null @@ -1,61 +0,0 @@ -import typing -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session -from contextlib import contextmanager, asynccontextmanager -from ..utils import asyncify - - -class Alchemy: - """A wrapper around SQLAlchemy declarative that allows to use multiple databases at once while maintaining a single table-class for both of them.""" - - def __init__(self, database_uri: str, tables: typing.Set): - """Create a new Alchemy object. - - Args: - database_uri: The uri of the database, as described at https://docs.sqlalchemy.org/en/13/core/engines.html . - tables: The set of tables to be created and used in the selected database. Check the tables submodule for more details. - """ - if database_uri.startswith("sqlite"): - raise NotImplementedError("Support for sqlite databases is currently missing") - self.engine = create_engine(database_uri) - self.Base = declarative_base(bind=self.engine) - self.Session = sessionmaker(bind=self.engine) - self._create_tables(tables) - - def _create_tables(self, tables: typing.Set): - for table in tables: - name = table.__name__ - try: - self.__getattribute__(name) - except AttributeError: - # Actually the intended result - # TODO: here is the problem! - self.__setattr__(name, type(name, (self.Base, table), {})) - else: - raise NameError(f"{name} is a reserved name and can't be used as a table name") - self.Base.metadata.create_all() - - @contextmanager - def session_cm(self): - """Use Alchemy as a context manager (to be used in with statements).""" - session = self.Session() - try: - yield session - except Exception: - session.rollback() - raise - finally: - session.close() - - @asynccontextmanager - async def session_acm(self): - """Use Alchemy as a asyncronous context manager (to be used in async with statements).""" - session = await asyncify(self.Session) - try: - yield session - except Exception: - session.rollback() - raise - finally: - session.close() diff --git a/royalnet/database/databaseconfig.py b/royalnet/database/databaseconfig.py deleted file mode 100644 index acb689d7..00000000 --- a/royalnet/database/databaseconfig.py +++ /dev/null @@ -1,15 +0,0 @@ -import typing - - -class DatabaseConfig: - """The configuration to be used for the :py:class:`royalnet.database.Alchemy` component of :py:class:`royalnet.bots.GenericBot`.""" - - def __init__(self, - database_uri: str, - master_table: typing.Type, - identity_table: typing.Type, - identity_column_name: str): - self.database_uri: str = database_uri - self.master_table: typing.Type = master_table - self.identity_table: typing.Type = identity_table - self.identity_column_name: str = identity_column_name diff --git a/royalnet/database/relationshiplinkchain.py b/royalnet/database/relationshiplinkchain.py deleted file mode 100644 index ef7b5446..00000000 --- a/royalnet/database/relationshiplinkchain.py +++ /dev/null @@ -1,22 +0,0 @@ -import typing -from sqlalchemy.inspection import inspect - - -def relationshiplinkchain(starting_class, ending_class) -> typing.Optional[tuple]: - """Find the path to follow to get from the starting table to the ending table.""" - inspected = set() - - def search(_mapper, chain): - inspected.add(_mapper) - if _mapper.class_ == ending_class: - return chain - relationships = _mapper.relationships - for _relationship in set(relationships): - if _relationship.mapper in inspected: - continue - result = search(_relationship.mapper, chain + (_relationship,)) - if len(result) != 0: - return result - return () - - return search(inspect(starting_class), tuple()) diff --git a/royalnet/error.py b/royalnet/error.py deleted file mode 100644 index 7a1ac5db..00000000 --- a/royalnet/error.py +++ /dev/null @@ -1,18 +0,0 @@ -import typing -import royalherald as rh - - -class RoyalnetRequestError(Exception): - """An error was raised while handling the Royalnet request. - - This exception contains the :py:class:`royalherald.ResponseFailure` that was returned by the other Link.""" - def __init__(self, error: rh.ResponseFailure): - self.error: rh.ResponseFailure = error - - @property - def args(self): - return f"{self.error.name}", f"{self.error.description}", f"{self.error.extra_info}" - - -class RoyalnetResponseError(Exception): - """The :py:class:`royalherald.Response` that was received is invalid.""" diff --git a/royalnet/herald/__init__.py b/royalnet/herald/__init__.py new file mode 100644 index 00000000..291c7088 --- /dev/null +++ b/royalnet/herald/__init__.py @@ -0,0 +1,26 @@ +from .config import Config +from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError +from .link import Link +from .package import Package +from .request import Request +from .response import Response, ResponseSuccess, ResponseFailure +from .server import Server +from .broadcast import Broadcast + + +__all__ = [ + "Config", + "HeraldError", + "ConnectionClosedError", + "LinkError", + "InvalidServerResponseError", + "ServerError", + "Link", + "Package", + "Request", + "Response", + "ResponseSuccess", + "ResponseFailure", + "Server", + "Broadcast", +] diff --git a/royalnet/herald/broadcast.py b/royalnet/herald/broadcast.py new file mode 100644 index 00000000..848aff9b --- /dev/null +++ b/royalnet/herald/broadcast.py @@ -0,0 +1,26 @@ +import typing + + +class Broadcast: + def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None): + super().__init__() + if msg_type is not None: + assert msg_type == self.__class__.__name__ + self.msg_type = self.__class__.__name__ + self.handler: str = handler + self.data: dict = data + + def to_dict(self): + return self.__dict__ + + @classmethod + def from_dict(cls, d: dict): + return cls(**d) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.handler == other.handler and self.data == other.data + return False + + def __repr__(self): + return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})" diff --git a/royalnet/herald/config.py b/royalnet/herald/config.py new file mode 100644 index 00000000..e74a8577 --- /dev/null +++ b/royalnet/herald/config.py @@ -0,0 +1,72 @@ +from typing import Optional + + +class Config: + def __init__(self, + name: str, + address: str, + port: int, + secret: str, + secure: bool = False, + path: str = "/" + ): + if ":" in name: + raise ValueError("Herald names cannot contain colons (:)") + self.name = name + + self.address = address + + if port < 0 or port > 65535: + raise ValueError("No such port") + self.port = port + + self.secure = secure + + if ":" in secret: + raise ValueError("Herald secrets cannot contain colons (:)") + self.secret = secret + + if not path.startswith("/"): + raise ValueError("Herald paths must start with a slash (/)") + self.path = path + + @property + def url(self): + return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}" + + def copy(self, + name: Optional[str] = None, + address: Optional[str] = None, + port: Optional[int] = None, + secret: Optional[str] = None, + secure: Optional[bool] = None, + path: Optional[str] = None): + """Create an exact copy of this configuration, but with different parameters.""" + return self.__class__(name=name if name else self.name, + address=address if address else self.address, + port=port if port else self.port, + secret=secret if secret else self.secret, + secure=secure if secure else self.secure, + path=path if path else self.path) + + def __repr__(self): + return f"" + + @classmethod + def from_config(cls, *, + name: str, + address: str, + port: int, + secret: str, + secure: bool = False, + path: str = "/", + enabled: ... = ... + ): + return cls( + name=name, + address=address, + port=port, + secret=secret, + secure=secure, + path=path + ) diff --git a/royalnet/herald/errors.py b/royalnet/herald/errors.py new file mode 100644 index 00000000..8c404697 --- /dev/null +++ b/royalnet/herald/errors.py @@ -0,0 +1,18 @@ +class HeraldError(Exception): + """A generic :mod:`royalnet.herald` error.""" + + +class LinkError(HeraldError): + """An error for something that happened in a :class:`Link`.""" + + +class ServerError(HeraldError): + """An error for something that happened in a :class:`Server`.""" + + +class ConnectionClosedError(LinkError): + """The :py:class:`Link`'s connection was closed unexpectedly. The link can't be used anymore.""" + + +class InvalidServerResponseError(LinkError): + """The :py:class:`Server` sent invalid data to the :class:`Link`.""" diff --git a/royalnet/herald/link.py b/royalnet/herald/link.py new file mode 100644 index 00000000..39b741ff --- /dev/null +++ b/royalnet/herald/link.py @@ -0,0 +1,186 @@ +import asyncio +import uuid +import functools +import logging as _logging +import typing +from .package import Package +from .request import Request +from .response import Response, ResponseSuccess, ResponseFailure +from .broadcast import Broadcast +from .errors import ConnectionClosedError, InvalidServerResponseError +from .config import Config + +try: + import websockets +except ImportError: + websockets = None + + +log = _logging.getLogger(__name__) + + +class PendingRequest: + def __init__(self, *, loop: asyncio.AbstractEventLoop = None): + if loop is None: + self.loop = asyncio.get_event_loop() + else: + self.loop = loop + self.event: asyncio.Event = asyncio.Event(loop=loop) + self.data: typing.Optional[dict] = None + + def __repr__(self): + if self.event.is_set(): + return f"<{self.__class__.__qualname__}: {self.data.__class__.__name__}>" + return f"<{self.__class__.__qualname__}>" + + def set(self, data): + self.data = data + self.event.set() + + +def requires_connection(func): + @functools.wraps(func) + async def new_func(self, *args, **kwargs): + await self.connect_event.wait() + return await func(self, *args, **kwargs) + return new_func + + +def requires_identification(func): + @functools.wraps(func) + async def new_func(self, *args, **kwargs): + await self.identify_event.wait() + return await func(self, *args, **kwargs) + return new_func + + +class Link: + def __init__(self, config: Config, request_handler, *, + loop: asyncio.AbstractEventLoop = None): + if websockets is None: + raise ImportError("'websockets' extra is not installed") + self.config: Config = config + self.nid: str = str(uuid.uuid4()) + self.websocket: typing.Optional["websockets.WebSocketClientProtocol"] = None + self.request_handler: typing.Callable[[typing.Union[Request, Broadcast]], + typing.Awaitable[Response]] = request_handler + self._pending_requests: typing.Dict[str, PendingRequest] = {} + if loop is None: + self._loop = asyncio.get_event_loop() + else: + self._loop = loop + self.error_event: asyncio.Event = asyncio.Event(loop=self._loop) + self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop) + self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop) + + def __repr__(self): + if self.identify_event.is_set(): + return f"<{self.__class__.__qualname__} (identified)>" + elif self.connect_event.is_set(): + return f"<{self.__class__.__qualname__} (connected)>" + elif self.error_event.is_set(): + return f"<{self.__class__.__qualname__} (error)>" + else: + return f"<{self.__class__.__qualname__} (disconnected)>" + + async def connect(self): + """Connect to the :class:`Server` at :attr:`.config.url`.""" + log.debug(f"Connecting to Herald Server at {self.config.url}...") + self.websocket = await websockets.connect(self.config.url, loop=self._loop) + self.connect_event.set() + log.debug(f"Connected!") + + @requires_connection + async def receive(self) -> Package: + """Recieve a :py:class:`Package` from the :py:class:`Server`. + + Raises: + :exc:`ConnectionClosedError` if the connection is closed.""" + try: + jbytes: bytes = await self.websocket.recv() + package: Package = Package.from_json_bytes(jbytes) + except websockets.ConnectionClosed: + self.error_event.set() + self.connect_event.clear() + self.identify_event.clear() + log.warning(f"Herald Server connection closed: {self.config.url}") + # What to do now? Let's just reraise. + raise ConnectionClosedError() + if self.identify_event.is_set() and package.destination != self.nid: + raise InvalidServerResponseError("Package is not addressed to this NetworkLink.") + log.debug(f"Received package: {package}") + return package + + @requires_connection + async def identify(self) -> None: + log.debug(f"Identifying...") + await self.websocket.send(f"Identify {self.nid}:{self.config.name}:{self.config.secret}") + response: Package = await self.receive() + if not response.source == "": + raise InvalidServerResponseError("Received a non-service package before identification.") + if "type" not in response.data: + raise InvalidServerResponseError("Missing 'type' in response data") + if response.data["type"] == "error": + raise ConnectionClosedError(f"Identification error: {response.data['type']}") + assert response.data["type"] == "success" + self.identify_event.set() + log.debug(f"Identified successfully!") + + @requires_identification + async def send(self, package: Package): + """Send a package to the :class:`Server`.""" + await self.websocket.send(package.to_json_bytes()) + log.debug(f"Sent package: {package}") + + @requires_identification + async def broadcast(self, destination: str, broadcast: Broadcast) -> None: + package = Package(broadcast.to_dict(), source=self.nid, destination=destination) + await self.send(package) + log.debug(f"Sent broadcast: {broadcast}") + + @requires_identification + async def request(self, destination: str, request: Request) -> Response: + if destination.startswith("*"): + raise ValueError("requests cannot have multiple destinations") + package = Package(request.to_dict(), source=self.nid, destination=destination) + request = PendingRequest(loop=self._loop) + self._pending_requests[package.source_conv_id] = request + await self.send(package) + log.debug(f"Sent request to {destination}: {request}") + await request.event.wait() + if request.data["type"] == "ResponseSuccess": + response: Response = ResponseSuccess.from_dict(request.data) + elif request.data["type"] == "ResponseFailure": + response: Response = ResponseFailure.from_dict(request.data) + else: + raise TypeError("Unknown response type") + log.debug(f"Received from {destination}: {request} -> {response}") + return response + + async def run(self): + """Blockingly run the Link.""" + log.debug(f"Running link: {self.config.name}") + if self.error_event.is_set(): + raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.") + while True: + if not self.connect_event.is_set(): + await self.connect() + if not self.identify_event.is_set(): + await self.identify() + package: Package = await self.receive() + # Package is a response + if package.destination_conv_id in self._pending_requests: + request = self._pending_requests[package.destination_conv_id] + request.set(package.data) + continue + # Package is a request + elif package.data["msg_type"] == "Request": + log.debug(f"Received request {package.source_conv_id}: {package}") + response: Response = await self.request_handler(Request.from_dict(package.data)) + response_package: Package = package.reply(response.to_dict()) + await self.send(response_package) + log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}") + # Package is a broadcast + elif package.data["msg_type"] == "Broadcast": + log.debug(f"Received broadcast {package.source_conv_id}: {package}") + await self.request_handler(Broadcast.from_dict(package.data)) diff --git a/royalnet/herald/package.py b/royalnet/herald/package.py new file mode 100644 index 00000000..77ca97a2 --- /dev/null +++ b/royalnet/herald/package.py @@ -0,0 +1,115 @@ +import json +import uuid +import typing + + +class Package: + """A data type with which a :py:class:`Link` communicates with a :py:class:`Server` or + another Link. + + Contains info about the source and the destination.""" + + def __init__(self, + data: dict, + *, + source: str, + destination: str, + source_conv_id: typing.Optional[str] = None, + destination_conv_id: typing.Optional[str] = None): + """Create a Package. + + Parameters: + data: The data that should be sent. + source: The ``nid`` of the node that created this Package. + destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. + Can also be the ``NULL`` value to send the message to nobody. + source_conv_id: The conversation id of the node that created this package. + Akin to the sequence number on IP packets. + destination_conv_id: The conversation id of the node that this Package is a reply to.""" + # TODO: something is not right in these type hints. Check them. + self.data: dict = data + self.source: str = source + self.source_conv_id: str = source_conv_id or str(uuid.uuid4()) + self.destination: str = destination + self.destination_conv_id: typing.Optional[str] = destination_conv_id + + def __repr__(self): + return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>" + + def __eq__(self, other): + if isinstance(other, Package): + return (self.data == other.data) and \ + (self.source == other.source) and \ + (self.destination == other.destination) and \ + (self.source_conv_id == other.source_conv_id) and \ + (self.destination_conv_id == other.destination_conv_id) + return False + + def reply(self, data) -> "Package": + """Reply to this :class:`Package` with another :class:`Package`. + + Parameters: + data: The data that should be sent. Usually a :class:`Request`. + + Returns: + The reply :class:`Package`.""" + return Package(data, + source=self.destination, + destination=self.source, + source_conv_id=self.destination_conv_id or str(uuid.uuid4()), + destination_conv_id=self.source_conv_id) + + @staticmethod + def from_dict(d) -> "Package": + """Create a :class:`Package` from a dictionary.""" + if "source" not in d: + raise ValueError("Missing source field") + if "nid" not in d["source"]: + raise ValueError("Missing source.nid field") + if "conv_id" not in d["source"]: + raise ValueError("Missing source.conv_id field") + if "destination" not in d: + raise ValueError("Missing destination field") + if "nid" not in d["destination"]: + raise ValueError("Missing destination.nid field") + if "conv_id" not in d["destination"]: + raise ValueError("Missing destination.conv_id field") + if "data" not in d: + raise ValueError("Missing data field") + return Package(d["data"], + source=d["source"]["nid"], + destination=d["destination"]["nid"], + source_conv_id=d["source"]["conv_id"], + destination_conv_id=d["destination"]["conv_id"]) + + def to_dict(self) -> dict: + """Convert the :class:`Package` into a dictionary.""" + return { + "source": { + "nid": self.source, + "conv_id": self.source_conv_id + }, + "destination": { + "nid": self.destination, + "conv_id": self.destination_conv_id + }, + "data": self.data + } + + @staticmethod + def from_json_string(string: str) -> "Package": + """Create a :class:`Package` from a JSON string.""" + return Package.from_dict(json.loads(string)) + + def to_json_string(self) -> str: + """Convert the :class:`Package` into a JSON string.""" + return json.dumps(self.to_dict()) + + @staticmethod + def from_json_bytes(b: bytes) -> "Package": + """Create a :class:`Package` from UTF-8-encoded JSON bytes.""" + return Package.from_json_string(str(b, encoding="utf8")) + + def to_json_bytes(self) -> bytes: + """Convert the :class:`Package` into UTF-8-encoded JSON bytes.""" + return bytes(self.to_json_string(), encoding="utf8") diff --git a/royalnet/herald/request.py b/royalnet/herald/request.py new file mode 100644 index 00000000..1d43e408 --- /dev/null +++ b/royalnet/herald/request.py @@ -0,0 +1,30 @@ +import typing + + +class Request: + """A request sent from a :class:`Link` to another. + + It contains the name of the requested handler, in addition to the data.""" + + def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None): + super().__init__() + if msg_type is not None: + assert msg_type == self.__class__.__name__ + self.msg_type = self.__class__.__name__ + self.handler: str = handler + self.data: dict = data + + def to_dict(self): + return self.__dict__ + + @classmethod + def from_dict(cls, d: dict): + return cls(**d) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.handler == other.handler and self.data == other.data + return False + + def __repr__(self): + return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})" diff --git a/royalnet/herald/response.py b/royalnet/herald/response.py new file mode 100644 index 00000000..73d830bb --- /dev/null +++ b/royalnet/herald/response.py @@ -0,0 +1,50 @@ +import typing + + +class Response: + """A base class to be inherited by all other response types.""" + + def to_dict(self) -> dict: + """Prepare the Response to be sent by converting it to a JSONable :py:class:`dict`.""" + return { + "type": self.__class__.__name__, + **self.__dict__ + } + + def __eq__(self, other): + if isinstance(other, Response): + return self.to_dict() == other.to_dict() + return False + + @classmethod + def from_dict(cls, d: dict) -> "Response": + """Recreate the response from a received :py:class:`dict`.""" + # Ignore type in dict + del d["type"] + # noinspection PyArgumentList + return cls(**d) + + +class ResponseSuccess(Response): + """A response to a successful :py:class:`Request`.""" + + def __init__(self, data: typing.Optional[dict] = None): + if data is None: + self.data = {} + else: + self.data = data + + def __repr__(self): + return f"{self.__class__.__qualname__}(data={self.data})" + + +class ResponseFailure(Response): + """A response to a invalid :py:class:`Request`.""" + + def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None): + self.name: str = name + self.description: str = description + self.extra_info: typing.Optional[dict] = extra_info + + def __repr__(self): + return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})" diff --git a/royalnet/herald/server.py b/royalnet/herald/server.py new file mode 100644 index 00000000..6c672a2f --- /dev/null +++ b/royalnet/herald/server.py @@ -0,0 +1,169 @@ +from typing import * +import re +import datetime +import uuid +import asyncio +import logging as _logging +import royalnet.utils as ru +from .package import Package +from .config import Config + +try: + import coloredlogs +except ImportError: + coloredlogs = None + +try: + import websockets +except ImportError: + websockets = None + + +log = _logging.getLogger(__name__) + + +class ConnectedClient: + """The :py:class:`Server`-side representation of a connected :py:class:`Link`.""" + def __init__(self, socket: "websockets.WebSocketServerProtocol"): + self.socket: "websockets.WebSocketServerProtocol" = socket + self.nid: Optional[str] = None + self.link_type: Optional[str] = None + self.connection_datetime: datetime.datetime = datetime.datetime.now() + + def __repr__(self): + return f"<{self.__class__.__qualname__} {self.nid}>" + + @property + def is_identified(self) -> bool: + """Has the client sent a valid identification package?""" + return bool(self.nid) + + async def send_service(self, msg_type: str, message: str): + await self.send(Package({"type": msg_type, "service": message}, + source="", + destination=self.nid)) + + async def send(self, package: Package): + """Send a :py:class:`Package` to the :py:class:`Link`.""" + await self.socket.send(package.to_json_bytes()) + + +class Server: + def __init__(self, config: Config, *, loop: asyncio.AbstractEventLoop = None): + self.config: Config = config + self.identified_clients: List[ConnectedClient] = [] + self.loop = loop + + def __repr__(self): + return f"<{self.__class__.__qualname__}>" + + def find_client(self, *, nid: str = None, link_type: str = None) -> List[ConnectedClient]: + assert not (nid and link_type) + if nid: + matching = [client for client in self.identified_clients if client.nid == nid] + assert len(matching) <= 1 + return matching + if link_type: + matching = [client for client in self.identified_clients if client.link_type == link_type] + return matching or [] + + async def listener(self, websocket: "websockets.server.WebSocketServerProtocol", path): + connected_client = ConnectedClient(websocket) + # Wait for identification + identify_msg = await websocket.recv() + log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.") + if not isinstance(identify_msg, str): + log.warning(f"Failed Herald identification: {websocket.remote_address[0]}:{websocket.remote_address[1]}") + await connected_client.send_service("error", "Invalid identification message (not a str)") + return + identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg) + if identification is None: + log.warning(f"Failed Herald identification: {websocket.remote_address[0]}:{websocket.remote_address[1]}") + await connected_client.send_service("error", "Invalid identification message (regex failed)") + return + secret = identification.group(3) + if secret != self.config.secret: + log.warning(f"Invalid Herald secret: {websocket.remote_address[0]}:{websocket.remote_address[1]}") + await connected_client.send_service("error", "Invalid secret") + return + # Identification successful + connected_client.nid = identification.group(1) + connected_client.link_type = identification.group(2) + log.info(f"Joined the Herald: {websocket.remote_address[0]}:{websocket.remote_address[1]}" + f" ({connected_client.link_type})") + self.identified_clients.append(connected_client) + await connected_client.send_service("success", "Identification successful!") + log.debug(f"{connected_client.nid}'s identification confirmed.") + # Main loop + while True: + # Receive packages + raw_bytes = await websocket.recv() + package: Package = Package.from_json_bytes(raw_bytes) + log.debug(f"Received package: {package}") + # Check if the package destination is the server itself. + if package.destination == "": + # TODO: do stuff + pass + # Otherwise, route the package to its destination + # noinspection PyAsyncCall + self.loop.create_task(self.route_package(package)) + + def find_destination(self, package: Package) -> List[ConnectedClient]: + """Find a list of destinations for the package. + + Parameters: + package: The package to find the destination of. + + Returns: + A :class:`list` of :class:`ConnectedClient` to send the package to.""" + # Parse destination + # Is it nothing? + if package.destination == "": + return [] + # Is it all possible destinations? + if package.destination == "*": + return self.identified_clients + # Is it a valid nid? + try: + destination = str(uuid.UUID(package.destination)) + except ValueError: + pass + else: + return self.find_client(nid=destination) + # Is it a link_type? + return self.find_client(link_type=package.destination) + + async def route_package(self, package: Package) -> None: + """Executed every time a :class:`Package` is received and must be routed somewhere.""" + destinations = self.find_destination(package) + log.debug(f"Routing package: {package} -> {destinations}") + for destination in destinations: + # This may have some consequences + specific_package = Package(package.data, + source=package.source, + destination=destination.nid, + source_conv_id=package.source_conv_id, + destination_conv_id=package.destination_conv_id) + await destination.send(specific_package) + + def serve(self): + if self.config.secure: + raise Exception("Secure servers aren't supported yet") + log.debug(f"Serving on {self.config.url}") + try: + self.loop.run_until_complete(self.run()) + except OSError as e: + log.fatal(f"OSError: {e}") + self.loop.run_forever() + + async def run(self): + await websockets.serve(self.listener, + host=self.config.address, + port=self.config.port, + loop=self.loop) + + def run_blocking(self, logging_cfg: Dict[str, Any]): + ru.init_logging(logging_cfg) + if self.loop is None: + self.loop = asyncio.get_event_loop() + self.serve() diff --git a/royalnet/packs/__init__.py b/royalnet/packs/__init__.py deleted file mode 100644 index 0b86d1d7..00000000 --- a/royalnet/packs/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import common - -__all__ = [ - "common", -] diff --git a/royalnet/packs/common/commands/ping.py b/royalnet/packs/common/commands/ping.py deleted file mode 100644 index 25f2b9bd..00000000 --- a/royalnet/packs/common/commands/ping.py +++ /dev/null @@ -1,10 +0,0 @@ -from royalnet.commands import * - - -class PingCommand(Command): - name: str = "ping" - - description: str = "Get a pong response." - - async def run(self, args: CommandArgs, data: CommandData) -> None: - await data.reply("📠Pong!") diff --git a/royalnet/packs/common/commands/version.py b/royalnet/packs/common/commands/version.py deleted file mode 100644 index 1a66f9e3..00000000 --- a/royalnet/packs/common/commands/version.py +++ /dev/null @@ -1,14 +0,0 @@ -from royalnet.commands import * -from royalnet.version import semantic - - -class VersionCommand(Command): - name: str = "version" - - description: str = "Get the current Royalnet version." - - async def run(self, args: CommandArgs, data: CommandData) -> None: - message = f"â„¹ï¸ Royalnet {semantic}\n" - if "69" in message: - message += "(Nice.)" - await data.reply(message) diff --git a/royalnet/serf/__init__.py b/royalnet/serf/__init__.py new file mode 100644 index 00000000..57cc18c3 --- /dev/null +++ b/royalnet/serf/__init__.py @@ -0,0 +1,10 @@ +from .serf import Serf +from .errors import SerfError +from . import telegram, discord + +__all__ = [ + "Serf", + "SerfError", + "telegram", + "discord", +] diff --git a/royalnet/serf/discord/__init__.py b/royalnet/serf/discord/__init__.py new file mode 100644 index 00000000..af5bb41e --- /dev/null +++ b/royalnet/serf/discord/__init__.py @@ -0,0 +1,17 @@ +"""A :class:`Serf` implementation for Discord. + +It is pretty unstable, compared to the rest of the bot, but it *should* work.""" + +from .escape import escape +from .discordserf import DiscordSerf +from .playable import Playable +from .playableytdqueue import PlayableYTDQueue +from .voiceplayer import VoicePlayer + +__all__ = [ + "escape", + "DiscordSerf", + "Playable", + "PlayableYTDQueue", + "VoicePlayer", +] diff --git a/royalnet/serf/discord/discordserf.py b/royalnet/serf/discord/discordserf.py new file mode 100644 index 00000000..52e889c5 --- /dev/null +++ b/royalnet/serf/discord/discordserf.py @@ -0,0 +1,235 @@ +import asyncio +import logging +import warnings +from typing import * +import royalnet.backpack as rb +from royalnet.commands import * +from royalnet.utils import asyncify +from royalnet.serf import Serf +from .escape import escape +from .voiceplayer import VoicePlayer + + +try: + import discord +except ImportError: + discord = None + +try: + from sqlalchemy.orm.session import Session +except ImportError: + Session = None + +try: + from royalnet.herald import Config as HeraldConfig +except ImportError: + HeraldConfig = None + +log = logging.getLogger(__name__) + + +class DiscordSerf(Serf): + """A :class:`Serf` that connects to `Discord `_ as a bot.""" + interface_name = "discord" + + _identity_table = rb.tables.Discord + _identity_column = "discord_id" + + def __init__(self, + alchemy_cfg: Dict[str, Any], + herald_cfg: Dict[str, Any], + sentry_cfg: Dict[str, Any], + packs_cfg: Dict[str, Any], + serf_cfg: Dict[str, Any], + **_): + if discord is None: + raise ImportError("'discord' extra is not installed") + + super().__init__(alchemy_cfg=alchemy_cfg, + herald_cfg=herald_cfg, + sentry_cfg=sentry_cfg, + packs_cfg=packs_cfg, + serf_cfg=serf_cfg) + + self.token = serf_cfg["token"] + """The Discord bot token.""" + + self.Client = self.client_factory() + """The custom :class:`discord.Client` class that will be instantiated later.""" + + self.client = self.Client() + """The custom :class:`discord.Client` instance.""" + + self.voice_players: List[VoicePlayer] = [] + """A :class:`list` of the :class:`VoicePlayer` in use by this :class:`DiscordSerf`.""" + + def interface_factory(self) -> Type[CommandInterface]: + # noinspection PyPep8Naming + GenericInterface = super().interface_factory() + + # noinspection PyMethodParameters,PyAbstractClass + class DiscordInterface(GenericInterface): + name = self.interface_name + prefix = "!" + + return DiscordInterface + + def data_factory(self) -> Type[CommandData]: + # noinspection PyMethodParameters,PyAbstractClass + class DiscordData(CommandData): + def __init__(data, + interface: CommandInterface, + session, + loop: asyncio.AbstractEventLoop, + message: "discord.Message"): + super().__init__(interface=interface, session=session, loop=loop) + data.message = message + + async def reply(data, text: str): + await data.message.channel.send(escape(text)) + + async def get_author(data, error_if_none=False): + user: "discord.Member" = data.message.author + query = data.session.query(self.master_table) + for link in self.identity_chain: + query = query.join(link.mapper.class_) + query = query.filter(self.identity_column == user.id) + result = await asyncify(query.one_or_none) + if result is None and error_if_none: + raise CommandError("You must be registered to use this command.") + return result + + async def delete_invoking(data, error_if_unavailable=False): + await data.message.delete() + + return DiscordData + + async def handle_message(self, message: "discord.Message"): + """Handle a Discord message by calling a command if appropriate.""" + text = message.content + # Skip non-text messages + if not text: + return + # Skip non-command updates + if not text.startswith("!"): + return + # Skip bot messages + author: Union["discord.User"] = message.author + if author.bot: + return + # Find and clean parameters + command_text, *parameters = text.split(" ") + # Don't use a case-sensitive command name + command_name = command_text.lower() + # Find the command + try: + command = self.commands[command_name] + except KeyError: + # Skip the message + return + # Call the command + log.debug(f"Calling command '{command.name}'") + with message.channel.typing(): + # Open an alchemy session, if available + if self.alchemy is not None: + session = await asyncify(self.alchemy.Session) + else: + session = None + # Prepare data + data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message) + # Call the command + await self.call(command, data, parameters) + # Close the alchemy session + if session is not None: + await asyncify(session.close) + + def client_factory(self) -> Type["discord.Client"]: + """Create a custom class inheriting from :py:class:`discord.Client`.""" + # noinspection PyMethodParameters + class DiscordClient(discord.Client): + async def on_message(cli, message: "discord.Message"): + """Handle messages received by passing them to the handle_message method of the bot.""" + # TODO: keep reference to these tasks somewhere + self.loop.create_task(self.handle_message(message)) + + async def on_ready(cli) -> None: + """Change the bot presence to ``online`` when the bot is ready.""" + await cli.change_presence(status=discord.Status.online) + + return DiscordClient + + async def run(self): + await super().run() + await self.client.login(self.token) + await self.client.connect() + + def find_channel(self, + channel_type: Optional[Type["discord.abc.GuildChannel"]] = None, + name: Optional[str] = None, + guild: Optional["discord.Guild"] = None, + accessible_to: List["discord.User"] = None, + required_permissions: List[str] = None) -> Optional["discord.abc.GuildChannel"]: + """Find the best channel matching all requests. + + In case multiple channels match all requests, return the one with the most members connected. + + Args: + channel_type: Filter channels by type (select only :class:`discord.VoiceChannel`, + :class:`discord.TextChannel`, ...). + name: Filter channels by name starting with ``name`` (using :meth:`str.startswith`). + Note that some channel types don't have names; this check will be skipped for them. + guild: Filter channels by guild, keep only channels inside this one. + accessible_to: Filter channels by permissions, keeping only channels where *all* these users have + the required permissions. + required_permissions: Filter channels by permissions, keeping only channels where the users have *all* these + :class:`discord.Permissions`. + + Returns: + Either a :class:`~discord.abc.GuildChannel`, or :const:`None` if no channels were found.""" + warnings.warn("This function will be removed soon.", category=DeprecationWarning) + if accessible_to is None: + accessible_to = [] + if required_permissions is None: + required_permissions = [] + channels: List[discord.abc.GuildChannel] = [] + for ch in self.client.get_all_channels(): + if channel_type is not None and not isinstance(ch, channel_type): + continue + + if name is not None: + try: + ch_name: str = ch.name + if not ch_name.startswith(name): + continue + except AttributeError: + pass + + ch_guild: "discord.Guild" = ch.guild + if guild is not None and guild != ch_guild: + continue + + for user in accessible_to: + member: "discord.Member" = ch.guild.get_member(user.id) + if member is None: + continue + permissions: "discord.Permissions" = ch.permissions_for(member) + missing_perms = False + for permission in required_permissions: + if not permissions.__getattribute__(permission): + missing_perms = True + break + if missing_perms: + continue + + channels.append(ch) + + if len(channels) == 0: + return None + else: + # Give priority to channels with the most people + def people_count(c: discord.VoiceChannel): + return len(c.members) + + channels.sort(key=people_count, reverse=True) + + return channels[0] diff --git a/royalnet/serf/discord/errors.py b/royalnet/serf/discord/errors.py new file mode 100644 index 00000000..680e84e8 --- /dev/null +++ b/royalnet/serf/discord/errors.py @@ -0,0 +1,42 @@ +from ..errors import SerfError + + +class DiscordSerfError(SerfError): + """Base class for all :mod:`royalnet.serf.discord` errors.""" + + +class VoicePlayerError(DiscordSerfError): + """Base class for all :class:`VoicePlayer` errors.""" + + +class AlreadyConnectedError(VoicePlayerError): + """Base class for the "Already Connected" errors.""" + + +class PlayerAlreadyConnectedError(AlreadyConnectedError): + """The :class:`VoicePlayer` is already connected to voice. + + Access the :class:`discord.VoiceClient` through :attr:`VoicePlayer.voice_client`!""" + + +class GuildAlreadyConnectedError(AlreadyConnectedError): + """The :class:`discord.Client` is already connected to voice in a channel of this guild.""" + + +class OpusNotLoadedError(VoicePlayerError): + """The Opus library hasn't been loaded `as required + ` by :mod:`discord`.""" + + +class DiscordTimeoutError(VoicePlayerError): + """The websocket didn't get a response from the Discord voice servers in time.""" + + +class PlayerNotConnectedError(VoicePlayerError): + """The :class:`VoicePlayer` isn't connected to the Discord voice servers. + + Use :meth:`VoicePlayer.connect` first!""" + + +class PlayerAlreadyPlaying(VoicePlayerError): + """The :class:`VoicePlayer` is already playing audio and cannot start playing audio again.""" diff --git a/royalnet/serf/discord/escape.py b/royalnet/serf/discord/escape.py new file mode 100644 index 00000000..10fa9a26 --- /dev/null +++ b/royalnet/serf/discord/escape.py @@ -0,0 +1,18 @@ +def escape(string: str) -> str: + """Escape a string to be sent through Discord, and format it using RoyalCode. + + Warning: + Currently escapes everything, even items in code blocks.""" + return string.replace("*", "\\*") \ + .replace("_", "\\_") \ + .replace("`", "\\`") \ + .replace("[b]", "**") \ + .replace("[/b]", "**") \ + .replace("[i]", "_") \ + .replace("[/i]", "_") \ + .replace("[u]", "__") \ + .replace("[/u]", "__") \ + .replace("[c]", "`") \ + .replace("[/c]", "`") \ + .replace("[p]", "```") \ + .replace("[/p]", "```") diff --git a/royalnet/serf/discord/playable.py b/royalnet/serf/discord/playable.py new file mode 100644 index 00000000..e7f678a6 --- /dev/null +++ b/royalnet/serf/discord/playable.py @@ -0,0 +1,61 @@ +import logging +from typing import Optional, AsyncGenerator, Tuple, Any, Dict +try: + import discord +except ImportError: + discord = None + + +log = logging.getLogger(__name__) + + +class Playable: + """An abstract class representing something that can be played back in a :class:`VoicePlayer`.""" + def __init__(self): + """Create a :class:`Playable`. + + Warning: + Avoid using this method, as it does not initialize the generator! Use :meth:`.create` instead.""" + log.debug("Creating a Playable...") + self.generator: Optional[AsyncGenerator[Optional["discord.AudioSource"], + Tuple[Tuple[Any, ...], Dict[str, Any]]]] = self._generator() + + # PyCharm doesn't like what I'm doing here. + # noinspection PyTypeChecker + @classmethod + async def create(cls, *args, **kwargs): + """Create a :class:`Playable` and initialize its generator.""" + playable = cls(*args, **kwargs) + log.debug("Sending None to the generator...") + await playable.generator.asend(None) + log.debug("Playable ready!") + return playable + + async def next(self, *args, **kwargs) -> Optional["discord.AudioSource"]: + """Get the next :class:`discord.AudioSource` that should be played. + + Called when the :class:`Playable` is first attached to a :class:`VoicePlayer` and when a + :class:`discord.AudioSource` stops playing. + + Args and kwargs can be used to pass data to the generator. + + Returns: + :const:`None` if there is nothing available to play, otherwise the :class:`discord.AudioSource` that should + be played. + """ + log.debug("Getting next AudioSource...") + audio_source: Optional["discord.AudioSource"] = await self.generator.asend((args, kwargs,)) + log.debug(f"Next: {audio_source}") + return audio_source + + async def _generator(self) \ + -> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]: + """Create an async generator that returns the next source to be played; + it can take a args+kwargs tuple in input to optionally select a different source. + + Note: + For `weird Python reasons + `, the generator + should ``yield`` once before doing anything else.""" + yield + raise NotImplementedError() diff --git a/royalnet/serf/discord/playableytdqueue.py b/royalnet/serf/discord/playableytdqueue.py new file mode 100644 index 00000000..7d48c360 --- /dev/null +++ b/royalnet/serf/discord/playableytdqueue.py @@ -0,0 +1,45 @@ +import logging +from typing import Optional, List, AsyncGenerator, Tuple, Any, Dict +from royalnet.bard import YtdlDiscord +from .playable import Playable +try: + import discord +except ImportError: + discord = None + + +log = logging.getLogger(__name__) + + +class PlayableYTDQueue(Playable): + """A queue of :class:`YtdlDiscord` to be played in sequence.""" + def __init__(self, start_with: Optional[List[YtdlDiscord]] = None): + super().__init__() + self.contents: List[YtdlDiscord] = [] + if start_with is not None: + self.contents = [*self.contents, *start_with] + log.debug(f"Created new PlayableYTDQueue containing: {self.contents}") + + async def _generator(self) \ + -> AsyncGenerator[Optional["discord.AudioSource"], Tuple[Tuple[Any, ...], Dict[str, Any]]]: + yield + while True: + log.debug(f"Dequeuing an item...") + try: + # Try to get the first YtdlDiscord of the queue + ytd: YtdlDiscord = self.contents.pop(0) + except IndexError: + # If there isn't anything, yield None + log.debug(f"Nothing to dequeue, yielding None.") + yield None + continue + log.debug(f"Yielding FileAudioSource from: {ytd}") + # Create a FileAudioSource from the YtdlDiscord + # If the file hasn't been fetched / downloaded / converted yet, it will do so before yielding + async with ytd.spawn_audiosource() as fas: + # Yield the resulting AudioSource + yield fas + # Delete the YtdlDiscord file + log.debug(f"Deleting: {ytd}") + await ytd.delete_asap() + log.debug(f"Deleted successfully!") diff --git a/royalnet/serf/discord/voiceplayer.py b/royalnet/serf/discord/voiceplayer.py new file mode 100644 index 00000000..8d6d2466 --- /dev/null +++ b/royalnet/serf/discord/voiceplayer.py @@ -0,0 +1,103 @@ +import asyncio +import logging +from typing import Optional +from .errors import * +from .playable import Playable +try: + import discord +except ImportError: + discord = None + +log = logging.getLogger(__name__) + + +class VoicePlayer: + def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None): + self.voice_client: Optional["discord.VoiceClient"] = None + self.playing: Optional[Playable] = None + if loop is None: + self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + else: + self.loop = loop + + async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient": + """Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient` + that handles the connection. + + Args: + channel: The :class:`discord.VoiceChannel` to connect into. + + Returns: + The created :class:`discord.VoiceClient`. + (It will be stored in :attr:`VoicePlayer.voice_client` anyways!) + + Raises: + PlayerAlreadyConnectedError: + DiscordTimeoutError: + GuildAlreadyConnectedError: + OpusNotLoadedError: + """ + if self.voice_client is not None and self.voice_client.is_connected(): + raise PlayerAlreadyConnectedError() + log.debug(f"Connecting to: {channel}") + try: + self.voice_client = await channel.connect() + except asyncio.TimeoutError: + raise DiscordTimeoutError() + except discord.ClientException: + raise GuildAlreadyConnectedError() + except discord.opus.OpusNotLoaded: + raise OpusNotLoadedError() + return self.voice_client + + async def disconnect(self) -> None: + """Disconnect the :class:`VoicePlayer` from the channel where it is currently connected, and set + :attr:`.voice_client` to :const:`None`. + + Raises: + PlayerNotConnectedError: + """ + if self.voice_client is None or not self.voice_client.is_connected(): + raise PlayerNotConnectedError() + log.debug(f"Disconnecting...") + await self.voice_client.disconnect(force=True) + self.voice_client = None + + async def move(self, channel: "discord.VoiceChannel"): + """Move the :class:`VoicePlayer` to a different channel. + + This requires the :class:`VoicePlayer` to already be connected, and for the passed :class:`discord.VoiceChannel` + to be in the same :class:`discord.Guild` of the :class:`VoicePlayer`.""" + if self.voice_client is None or not self.voice_client.is_connected(): + raise PlayerNotConnectedError() + if self.voice_client.guild != channel.guild: + raise ValueError("Can't move between two guilds.") + log.debug(f"Moving to: {channel}") + await self.voice_client.move_to(channel) + + async def start(self): + """Start playing music on the :class:`discord.VoiceClient`. + + Info: + Doesn't pass any ``*args`` or ``**kwargs`` to the :class:`Playable`. + """ + if self.voice_client is None or not self.voice_client.is_connected(): + raise PlayerNotConnectedError() + if self.voice_client.is_playing(): + raise PlayerAlreadyPlaying() + log.debug("Getting next AudioSource...") + next_source: Optional["discord.AudioSource"] = await self.playing.next() + if next_source is None: + log.debug(f"Next source would be None, stopping here...") + return + log.debug(f"Next: {next_source}") + self.voice_client.play(next_source, after=self._playback_ended) + + def _playback_ended(self, error: Exception = None): + """An helper method that is called when the :attr:`.voice_client._player` has finished playing.""" + if error is not None: + # TODO: capture exception with Sentry + log.error(f"Error during playback: {error}") + return + # Create a new task to create + self.loop.create_task(self.start()) diff --git a/royalnet/serf/errors.py b/royalnet/serf/errors.py new file mode 100644 index 00000000..6a07c623 --- /dev/null +++ b/royalnet/serf/errors.py @@ -0,0 +1,2 @@ +class SerfError(Exception): + """Base class for all :mod:`royalnet.serf` errors.""" diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py new file mode 100644 index 00000000..b6f4cbb2 --- /dev/null +++ b/royalnet/serf/serf.py @@ -0,0 +1,329 @@ +import logging +import importlib +import asyncio as aio +from typing import * + +from sqlalchemy.schema import Table + +from royalnet.commands import * +import royalnet.utils as ru +import royalnet.alchemy as ra +import royalnet.backpack as rb +import royalnet.herald as rh + +try: + import sentry_sdk + from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration + from sentry_sdk.integrations.aiohttp import AioHttpIntegration + from sentry_sdk.integrations.logging import LoggingIntegration +except ImportError: + sentry_sdk = None + SqlalchemyIntegration = None + AioHttpIntegration = None + LoggingIntegration = None + +try: + import coloredlogs +except ImportError: + coloredlogs = None + +log = logging.getLogger(__name__) + + +class Serf: + """An abstract class, to be used as base to implement Royalnet bots on multiple interfaces (such as Telegram or + Discord).""" + interface_name = NotImplemented + + _master_table: type = rb.tables.User + _identity_table: type = NotImplemented + _identity_column: str = NotImplemented + + def __init__(self, + alchemy_cfg: Dict[str, Any], + herald_cfg: Dict[str, Any], + packs_cfg: Dict[str, Any], + **_): + + # Import packs + pack_names = packs_cfg["active"] + packs = {} + for pack_name in pack_names: + log.debug(f"Importing pack: {pack_name}") + try: + packs[pack_name] = importlib.import_module(pack_name) + except ImportError as e: + log.error(f"Error during the import of {pack_name}: {e}") + log.info(f"Packs: {len(packs)} imported") + + self.alchemy: Optional[ra.Alchemy] = None + """The :class:`Alchemy` object connecting this :class:`Serf` to a database.""" + + self.master_table: Optional[Table] = None + """The central table listing all users. It usually is :class:`User`.""" + + self.identity_table: Optional[Table] = None + """The identity table containing the interface data (such as the Telegram user data) and that is in a + many-to-one relationship with the master table.""" + + # TODO: I'm not sure what this is either + self.identity_column: Optional[str] = None + + # Alchemy + if ra.Alchemy is None: + log.info("Alchemy: not installed") + elif not alchemy_cfg["enabled"]: + log.info("Alchemy: disabled") + else: + # Find all tables + tables = set() + for pack in packs.values(): + try: + tables = tables.union(pack.available_tables) + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_tables` attribute.") + continue + # Create the Alchemy + self.init_alchemy(alchemy_cfg, tables) + log.info(f"Alchemy: {self.alchemy}") + + self.herald: Optional[rh.Link] = None + """The :class:`Link` object connecting the :class:`Serf` to the rest of the Herald network.""" + + self.herald_task: Optional[aio.Task] = None + """A reference to the :class:`asyncio.Task` that runs the :class:`Link`.""" + + self.events: Dict[str, Event] = {} + """A dictionary containing all :class:`Event` that can be handled by this :class:`Serf`.""" + + self.Interface: Type[CommandInterface] = self.interface_factory() + """The :class:`CommandInterface` class of this Serf.""" + + self.Data: Type[CommandData] = self.data_factory() + """The :class:`CommandData` class of this Serf.""" + + self.commands: Dict[str, Command] = {} + """The :class:`dict` connecting each command name to its :class:`Command` object.""" + + for pack_name in packs: + pack = packs[pack_name] + pack_cfg = packs_cfg.get(pack_name, {}) + try: + events = pack.available_events + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_events` attribute.") + else: + self.register_events(events, pack_cfg) + try: + commands = pack.available_commands + except AttributeError: + log.warning(f"Pack `{pack}` does not have the `available_commands` attribute.") + else: + self.register_commands(commands, pack_cfg) + log.info(f"Events: {len(self.events)} events") + log.info(f"Commands: {len(self.commands)} commands") + + if rh.Link is None: + log.info("Herald: not installed") + elif not herald_cfg["enabled"]: + log.info("Herald: disabled") + else: + self.init_herald(herald_cfg) + log.info(f"Herald: enabled") + + self.loop: Optional[aio.AbstractEventLoop] = None + """The event loop this Serf is running on.""" + + def init_alchemy(self, alchemy_cfg: Dict[str, Any], tables: Set[type]) -> None: + """Create and initialize the :class:`Alchemy` with the required tables, and find the link between the master + table and the identity table.""" + self.alchemy = ra.Alchemy(alchemy_cfg["database_url"], tables) + self.master_table = self.alchemy.get(self._master_table) + self.identity_table = self.alchemy.get(self._identity_table) + # This is fine, as Pycharm doesn't know that identity_table is a class and not an object + # noinspection PyArgumentList + self.identity_column = self.identity_table.__getattribute__(self.identity_table, self._identity_column) + + @property + def identity_chain(self) -> tuple: + """Find a relationship path starting from the master table and ending at the identity table, and return it.""" + return ra.table_dfs(self.master_table, self.identity_table) + + def interface_factory(self) -> Type[CommandInterface]: + """Create the :class:`CommandInterface` class for the Serf.""" + + # noinspection PyMethodParameters + class GenericInterface(CommandInterface): + alchemy: ra.Alchemy = self.alchemy + serf: "Serf" = self + + async def call_herald_event(ci, destination: str, event_name: str, **kwargs) -> Dict: + """Send a :class:`royalherald.Request` to a specific destination, and wait for a + :class:`royalherald.Response`.""" + if self.herald is None: + raise UnsupportedError("`royalherald` is not enabled on this serf.") + request: rh.Request = rh.Request(handler=event_name, data=kwargs) + response: rh.Response = await self.herald.request(destination=destination, request=request) + if isinstance(response, rh.ResponseFailure): + if response.name == "no_event": + raise CommandError(f"There is no event named {event_name} in {destination}.") + elif response.name == "exception_in_event": + # TODO: pretty sure there's a better way to do this + if response.extra_info["type"] == "CommandError": + raise CommandError(response.extra_info["message"]) + elif response.extra_info["type"] == "UserError": + raise UserError(response.extra_info["message"]) + elif response.extra_info["type"] == "InvalidInputError": + raise InvalidInputError(response.extra_info["message"]) + elif response.extra_info["type"] == "UnsupportedError": + raise UnsupportedError(response.extra_info["message"]) + elif response.extra_info["type"] == "ConfigurationError": + raise ConfigurationError(response.extra_info["message"]) + elif response.extra_info["type"] == "ExternalError": + raise ExternalError(response.extra_info["message"]) + else: + raise ValueError(f"Herald action call returned invalid error:\n" + f"[p]{response}[/p]") + elif isinstance(response, rh.ResponseSuccess): + return response.data + else: + raise ValueError(f"Other Herald Link returned unknown response:\n" + f"[p]{response}[/p]") + + return GenericInterface + + def data_factory(self) -> Type[CommandData]: + """Create the :class:`CommandData` for the Serf.""" + raise NotImplementedError() + + def register_commands(self, commands: List[Type[Command]], pack_cfg: Dict[str, Any]) -> None: + """Initialize and register all commands passed as argument.""" + # Instantiate the Commands + for SelectedCommand in commands: + # Create a new interface + interface = self.Interface(cfg=pack_cfg) + # Try to instantiate the command + try: + command = SelectedCommand(interface) + except Exception as e: + log.error(f"Skipping: " + f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.") + ru.sentry_exc(e) + continue + # Link the interface to the command + interface.command = command + # Warn if the command would be overriding something + if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands: + log.info(f"Overriding (already defined): " + f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") + else: + log.debug(f"Registering: " + f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}") + # Register the command in the commands dict + self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command + # Register aliases, but don't override anything + for alias in SelectedCommand.aliases: + if f"{interface.prefix}{alias}" not in self.commands: + log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") + self.commands[f"{interface.prefix}{alias}"] = \ + self.commands[f"{interface.prefix}{SelectedCommand.name}"] + else: + log.warning( + f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}") + + def init_herald(self, herald_cfg: Dict[str, Any]): + """Create a :class:`Link` and bind :class:`Event`.""" + herald_cfg["name"] = self.interface_name + self.herald: rh.Link = rh.Link(rh.Config.from_config(**herald_cfg), self.network_handler) + + def register_events(self, events: List[Type[Event]], pack_cfg: Dict[str, Any]): + for SelectedEvent in events: + # Create a new interface + interface = self.Interface(cfg=pack_cfg) + # Initialize the event + try: + event = SelectedEvent(interface) + except Exception as e: + log.error(f"Skipping: " + f"{SelectedEvent.__qualname__} - {e.__class__.__qualname__} in the initialization.") + ru.sentry_exc(e) + continue + # Register the event + if SelectedEvent.name in self.events: + log.warning(f"Overriding (already defined): {SelectedEvent.__qualname__} -> {SelectedEvent.name}") + else: + log.debug(f"Registering: {SelectedEvent.__qualname__} -> {SelectedEvent.name}") + self.events[SelectedEvent.name] = event + + async def network_handler(self, message: Union[rh.Request, rh.Broadcast]) -> rh.Response: + try: + event: Event = self.events[message.handler] + except KeyError: + log.warning(f"No event for '{message.handler}'") + return rh.ResponseFailure("no_event", f"This serf does not have any event for {message.handler}.") + log.debug(f"Event called: {event.name}") + if isinstance(message, rh.Request): + try: + response_data = await event.run(**message.data) + return rh.ResponseSuccess(data=response_data) + except Exception as e: + ru.sentry_exc(e) + return rh.ResponseFailure("exception_in_event", + f"An exception was raised in the event for '{message.handler}'.", + extra_info={ + "type": e.__class__.__qualname__, + "message": str(e) + }) + elif isinstance(message, rh.Broadcast): + await event.run(**message.data) + + async def call(self, command: Command, data: CommandData, parameters: List[str]): + log.info(f"Calling command: {command.name}") + try: + # Run the command + await command.run(CommandArgs(parameters), data) + except InvalidInputError as e: + await data.reply(f"âš ï¸ {e.message}\n" + f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]") + except UserError as e: + await data.reply(f"âš ï¸ {e.message}") + except UnsupportedError as e: + await data.reply(f"âš ï¸ {e.message}") + except ExternalError as e: + await data.reply(f"âš ï¸ {e.message}") + except ConfigurationError as e: + await data.reply(f"âš ï¸ {e.message}") + except CommandError as e: + await data.reply(f"âš ï¸ {e.message}") + except Exception as e: + ru.sentry_exc(e) + error_message = f"â›”ï¸ [b]{e.__class__.__name__}[/b]\n" + '\n'.join(e.args) + await data.reply(error_message) + + async def run(self): + """A coroutine that starts the event loop and handles command calls.""" + self.herald_task = self.loop.create_task(self.herald.run()) + # OVERRIDE THIS METHOD! + + @classmethod + def run_process(cls, **kwargs): + """Blockingly create and run the Serf. + + This should be used as the target of a :class:`multiprocessing.Process`.""" + ru.init_logging(kwargs["logging_cfg"]) + + if kwargs["sentry_cfg"] is None or not kwargs["sentry_cfg"]["enabled"]: + log.info("Sentry: disabled") + else: + try: + ru.init_sentry(kwargs["sentry_cfg"]) + except ImportError: + log.info("Sentry: not installed") + + serf = cls(**kwargs) + + serf.loop = aio.get_event_loop() + try: + serf.loop.run_until_complete(serf.run()) + except Exception as e: + ru.sentry_exc(e, level="fatal") diff --git a/royalnet/serf/telegram/__init__.py b/royalnet/serf/telegram/__init__.py new file mode 100644 index 00000000..0e94ca9f --- /dev/null +++ b/royalnet/serf/telegram/__init__.py @@ -0,0 +1,7 @@ +from .escape import escape +from .telegramserf import TelegramSerf + +__all__ = [ + "escape", + "TelegramSerf" +] \ No newline at end of file diff --git a/royalnet/serf/telegram/escape.py b/royalnet/serf/telegram/escape.py new file mode 100644 index 00000000..6833ef6d --- /dev/null +++ b/royalnet/serf/telegram/escape.py @@ -0,0 +1,17 @@ +def escape(string: str) -> str: + """Escape a string to be sent through Telegram (as HTML), and format it using RoyalCode. + + Warning: + Currently escapes everything, even items in code blocks.""" + return string.replace("<", "<") \ + .replace(">", ">") \ + .replace("[b]", "") \ + .replace("[/b]", "") \ + .replace("[i]", "") \ + .replace("[/i]", "") \ + .replace("[u]", "") \ + .replace("[/u]", "") \ + .replace("[c]", "") \ + .replace("[/c]", "") \ + .replace("[p]", "
") \
+        .replace("[/p]", "
") diff --git a/royalnet/serf/telegram/telegramserf.py b/royalnet/serf/telegram/telegramserf.py new file mode 100644 index 00000000..72111d55 --- /dev/null +++ b/royalnet/serf/telegram/telegramserf.py @@ -0,0 +1,250 @@ +import logging +import asyncio as aio +from typing import * +from royalnet.commands import * +from royalnet.utils import asyncify +import royalnet.backpack as rb +from .escape import escape +from ..serf import Serf + +try: + import telegram + import urllib3 + from telegram.utils.request import Request as TRequest +except ImportError: + telegram = None + urllib3 = None + TRequest = None + +try: + from sqlalchemy.orm.session import Session +except ImportError: + Session = None + +log = logging.getLogger(__name__) + + +class TelegramSerf(Serf): + """A Serf that connects to `Telegram `_ as a bot.""" + interface_name = "telegram" + + _identity_table = rb.tables.Telegram + _identity_column = "tg_id" + + def __init__(self, + alchemy_cfg: Dict[str, Any], + herald_cfg: Dict[str, Any], + sentry_cfg: Dict[str, Any], + packs_cfg: Dict[str, Any], + serf_cfg: Dict[str, Any], + **_): + if telegram is None: + raise ImportError("'telegram' extra is not installed") + + super().__init__(alchemy_cfg=alchemy_cfg, + herald_cfg=herald_cfg, + sentry_cfg=sentry_cfg, + packs_cfg=packs_cfg, + serf_cfg=serf_cfg) + + self.client = telegram.Bot(serf_cfg["token"], + request=TRequest(serf_cfg["pool_size"], + read_timeout=serf_cfg["read_timeout"])) + """The :class:`telegram.Bot` instance that will be used from the Serf.""" + + self.update_offset: int = -100 + """The current `update offset `_.""" + + @staticmethod + async def api_call(f: Callable, *args, **kwargs) -> Optional: + """Call a :class:`telegram.Bot` method safely, without getting a mess of errors raised. + + The method may return None if it was decided that the call should be skipped.""" + while True: + try: + return await asyncify(f, *args, **kwargs) + except telegram.error.TimedOut as error: + log.debug(f"Timed out during {f.__qualname__} (retrying immediatly): {error}") + continue + except telegram.error.NetworkError as error: + log.debug(f"Network error during {f.__qualname__} (skipping): {error}") + break + except telegram.error.Unauthorized as error: + log.info(f"Unauthorized to run {f.__qualname__} (skipping): {error}") + break + except telegram.error.RetryAfter as error: + log.warning(f"Rate limited during {f.__qualname__} (retrying in 15s): {error}") + await aio.sleep(15) + continue + except urllib3.exceptions.HTTPError as error: + log.warning(f"urllib3 HTTPError during {f.__qualname__} (retrying in 15s): {error}") + await aio.sleep(15) + continue + except Exception as error: + log.error(f"{error.__class__.__qualname__} during {f} (skipping): {error}") + TelegramSerf.sentry_exc(error) + break + return None + + def interface_factory(self) -> Type[CommandInterface]: + # noinspection PyPep8Naming + GenericInterface = super().interface_factory() + + # noinspection PyMethodParameters + class TelegramInterface(GenericInterface): + name = self.interface_name + prefix = "/" + + return TelegramInterface + + def data_factory(self) -> Type[CommandData]: + # noinspection PyMethodParameters + class TelegramData(CommandData): + def __init__(data, + interface: CommandInterface, + session, + loop: aio.AbstractEventLoop, + update: telegram.Update): + super().__init__(interface=interface, session=session, loop=loop) + data.update = update + + async def reply(data, text: str): + await self.api_call(data.update.effective_chat.send_message, + escape(text), + parse_mode="HTML", + disable_web_page_preview=True) + + async def get_author(data, error_if_none=False): + if data.update.message is not None: + user: telegram.User = data.update.message.from_user + elif data.update.callback_query is not None: + user: telegram.User = data.update.callback_query.from_user + else: + raise CommandError("Command caller can not be determined") + if user is None: + if error_if_none: + raise CommandError("No command caller for this message") + return None + query = data.session.query(self.master_table) + for link in self.identity_chain: + query = query.join(link.mapper.class_) + query = query.filter(self.identity_column == user.id) + result = await asyncify(query.one_or_none) + if result is None and error_if_none: + raise CommandError("Command caller is not registered") + return result + + async def delete_invoking(data, error_if_unavailable=False) -> None: + message: telegram.Message = data.update.message + await self.api_call(message.delete) + + return TelegramData + + async def handle_update(self, update: telegram.Update): + """Delegate :class:`telegram.Update` handling to the correct message type submethod.""" + + if update.message is not None: + await self.handle_message(update) + elif update.edited_message is not None: + pass + elif update.channel_post is not None: + pass + elif update.edited_channel_post is not None: + pass + elif update.inline_query is not None: + pass + elif update.chosen_inline_result is not None: + pass + elif update.callback_query is not None: + pass + elif update.shipping_query is not None: + pass + elif update.pre_checkout_query is not None: + pass + elif update.poll is not None: + pass + else: + log.warning(f"Unknown update type: {update}") + + async def handle_message(self, update: telegram.Update): + """What should be done when a :class:`telegram.Message` is received?""" + message: telegram.Message = update.message + text: str = message.text + # Try getting the caption instead + if text is None: + text: str = message.caption + # No text or caption, ignore the message + if text is None: + return + # Skip non-command updates + if not text.startswith("/"): + return + # Find and clean parameters + command_text, *parameters = text.split(" ") + command_name = command_text.replace(f"@{self.client.username}", "").lower() + # Find the command + try: + command = self.commands[command_name] + except KeyError: + # Skip the message + return + # Send a typing notification + await self.api_call(update.message.chat.send_action, telegram.ChatAction.TYPING) + # Prepare data + if self.alchemy is not None: + session = await asyncify(self.alchemy.Session) + else: + session = None + # Prepare data + data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update) + # Call the command + await self.call(command, data, parameters) + # Close the alchemy session + if session is not None: + await asyncify(session.close) + + async def handle_edited_message(self, update: telegram.Update): + pass + + async def handle_channel_post(self, update: telegram.Update): + pass + + async def handle_edited_channel_post(self, update: telegram.Update): + pass + + async def handle_inline_query(self, update: telegram.Update): + pass + + async def handle_chosen_inline_result(self, update: telegram.Update): + pass + + async def handle_callback_query(self, update: telegram.Update): + pass + + async def handle_shipping_query(self, update: telegram.Update): + pass + + async def handle_pre_checkout_query(self, update: telegram.Update): + pass + + async def handle_poll(self, update: telegram.Update): + pass + + async def run(self): + await super().run() + while True: + # Get the latest 100 updates + last_updates: List[telegram.Update] = await self.api_call(self.client.get_updates, + offset=self.update_offset, + timeout=60, + read_latency=5.0) + # Handle updates + for update in last_updates: + # TODO: don't lose the reference to the task + # noinspection PyAsyncCall + self.loop.create_task(self.handle_update(update)) + # Recalculate offset + try: + self.update_offset = last_updates[-1].update_id + 1 + except IndexError: + pass diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 9627780d..d8a107e6 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -1,28 +1,27 @@ -"""Miscellaneous useful functions and classes.""" - from .asyncify import asyncify -from .escaping import telegram_escape, discord_escape from .safeformat import safeformat -from .classdictjanitor import cdj -from .sleepuntil import sleep_until -from .formatters import andformat, plusformat, fileformat, ytdldateformat, numberemojiformat, splitstring, ordinalformat +from .sleep_until import sleep_until +from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat from .urluuid import to_urluuid, from_urluuid +from .multilock import MultiLock +from .fileaudiosource import FileAudioSource +from .sentry import init_sentry, sentry_exc +from .log import init_logging __all__ = [ "asyncify", "safeformat", - "cdj", "sleep_until", - "plusformat", "andformat", - "plusformat", - "fileformat", + "underscorize", "ytdldateformat", "numberemojiformat", - "telegram_escape", - "discord_escape", - "splitstring", "ordinalformat", "to_urluuid", "from_urluuid", + "MultiLock", + "FileAudioSource", + "init_sentry", + "sentry_exc", + "init_logging", ] diff --git a/royalnet/utils/__pycache__/__init__.cpython-37.pyc b/royalnet/utils/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 00000000..120f6285 Binary files /dev/null and b/royalnet/utils/__pycache__/__init__.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/asyncify.cpython-37.pyc b/royalnet/utils/__pycache__/asyncify.cpython-37.pyc new file mode 100644 index 00000000..13219a7d Binary files /dev/null and b/royalnet/utils/__pycache__/asyncify.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/classdictjanitor.cpython-37.pyc b/royalnet/utils/__pycache__/classdictjanitor.cpython-37.pyc new file mode 100644 index 00000000..bfa1d2ae Binary files /dev/null and b/royalnet/utils/__pycache__/classdictjanitor.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/escaping.cpython-37.pyc b/royalnet/utils/__pycache__/escaping.cpython-37.pyc new file mode 100644 index 00000000..95139a78 Binary files /dev/null and b/royalnet/utils/__pycache__/escaping.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/formatters.cpython-37.pyc b/royalnet/utils/__pycache__/formatters.cpython-37.pyc new file mode 100644 index 00000000..5366fc30 Binary files /dev/null and b/royalnet/utils/__pycache__/formatters.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/parse5etoolsentry.cpython-37.pyc b/royalnet/utils/__pycache__/parse5etoolsentry.cpython-37.pyc new file mode 100644 index 00000000..40a63f09 Binary files /dev/null and b/royalnet/utils/__pycache__/parse5etoolsentry.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/safeformat.cpython-37.pyc b/royalnet/utils/__pycache__/safeformat.cpython-37.pyc new file mode 100644 index 00000000..fc4cd98c Binary files /dev/null and b/royalnet/utils/__pycache__/safeformat.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/sleepuntil.cpython-37.pyc b/royalnet/utils/__pycache__/sleepuntil.cpython-37.pyc new file mode 100644 index 00000000..6eef1655 Binary files /dev/null and b/royalnet/utils/__pycache__/sleepuntil.cpython-37.pyc differ diff --git a/royalnet/utils/__pycache__/urluuid.cpython-37.pyc b/royalnet/utils/__pycache__/urluuid.cpython-37.pyc new file mode 100644 index 00000000..7d49f90b Binary files /dev/null and b/royalnet/utils/__pycache__/urluuid.cpython-37.pyc differ diff --git a/royalnet/utils/asyncify.py b/royalnet/utils/asyncify.py index 92ae46a3..08ca349b 100644 --- a/royalnet/utils/asyncify.py +++ b/royalnet/utils/asyncify.py @@ -3,10 +3,11 @@ import functools import typing -async def asyncify(function: typing.Callable, *args, **kwargs): - """Convert a function into a coroutine. +async def asyncify(function: typing.Callable, *args, loop: typing.Optional[asyncio.AbstractEventLoop] = None, **kwargs): + """Asyncronously run the function in a different thread or process, preventing it from blocking the event loop. Warning: - The coroutine cannot be cancelled, and any attempts to do so will result in unexpected outputs.""" - loop = asyncio.get_event_loop() + If the function has side effects, it may behave strangely.""" + if not loop: + loop = asyncio.get_event_loop() return await loop.run_in_executor(None, functools.partial(function, *args, **kwargs)) diff --git a/royalnet/utils/classdictjanitor.py b/royalnet/utils/classdictjanitor.py deleted file mode 100644 index 3897f8dd..00000000 --- a/royalnet/utils/classdictjanitor.py +++ /dev/null @@ -1,19 +0,0 @@ -import typing - -def cdj(class_: typing.Any) -> dict: - """Return a dict of the class attributes without the ``__module__``, ``__dict__``, ``__weakref__`` and ``__doc__`` keys, to be used while generating dynamically SQLAlchemy declarative table classes. - - Parameters: - class_: The object that you want to dict-ify. - - Returns: - The class dict. - - Warning: - You can't dict-ify classes with ``__slots__``!""" - d = dict(class_.__dict__) - del d["__module__"] - del d["__dict__"] - del d["__weakref__"] - del d["__doc__"] - return d diff --git a/royalnet/utils/escaping.py b/royalnet/utils/escaping.py deleted file mode 100644 index 31a99ec7..00000000 --- a/royalnet/utils/escaping.py +++ /dev/null @@ -1,37 +0,0 @@ -def discord_escape(string: str) -> str: - """Escape a string to be sent through Discord, and format it using RoyalCode. - - Warning: - Currently escapes everything, even items in code blocks.""" - return string.replace("*", "\\*") \ - .replace("_", "\\_") \ - .replace("`", "\\`") \ - .replace("[b]", "**") \ - .replace("[/b]", "**") \ - .replace("[i]", "_") \ - .replace("[/i]", "_") \ - .replace("[u]", "__") \ - .replace("[/u]", "__") \ - .replace("[c]", "`") \ - .replace("[/c]", "`") \ - .replace("[p]", "```") \ - .replace("[/p]", "```") - - -def telegram_escape(string: str) -> str: - """Escape a string to be sent through Telegram, and format it using RoyalCode. - - Warning: - Currently escapes everything, even items in code blocks.""" - return string.replace("<", "<") \ - .replace(">", ">") \ - .replace("[b]", "") \ - .replace("[/b]", "") \ - .replace("[i]", "") \ - .replace("[/i]", "") \ - .replace("[u]", "") \ - .replace("[/u]", "") \ - .replace("[c]", "") \ - .replace("[/c]", "") \ - .replace("[p]", "
") \
-                 .replace("[/p]", "
") diff --git a/royalnet/audio/fileaudiosource.py b/royalnet/utils/fileaudiosource.py similarity index 58% rename from royalnet/audio/fileaudiosource.py rename to royalnet/utils/fileaudiosource.py index 110b6a53..3be44d40 100644 --- a/royalnet/audio/fileaudiosource.py +++ b/royalnet/utils/fileaudiosource.py @@ -1,4 +1,7 @@ -import discord +try: + import discord +except ImportError: + discord = None class FileAudioSource(discord.AudioSource): @@ -10,7 +13,12 @@ class FileAudioSource(discord.AudioSource): This AudioSource will consume (and close) the passed stream.""" def __init__(self, file): + """Create a FileAudioSource. + + Arguments: + file: the file to be played back.""" self.file = file + self._stopped = False def __repr__(self): if self.file.seekable(): @@ -25,17 +33,18 @@ class FileAudioSource(discord.AudioSource): ``False``.""" return False - def read(self): - """Reads 20ms worth of audio. + def stop(self): + """Stop the FileAudioSource. Once stopped, a FileAudioSource will immediatly stop reading more bytes from the + file.""" + self._stopped = True - If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so.""" - # If the stream is closed, it should stop playing immediatly - if self.file.closed: - return b"" + def read(self): + """Reads 20ms of audio. + + If the stream has ended, then return an empty :py:class:`bytes`-like object.""" data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) # If there is no more data to be streamed - if len(data) != discord.opus.Encoder.FRAME_SIZE: - # Close the file - self.file.close() + if self._stopped or len(data) != discord.opus.Encoder.FRAME_SIZE: + # Return that the stream has ended return b"" return data diff --git a/royalnet/utils/formatters.py b/royalnet/utils/formatters.py index 8d94648f..3c7c72f0 100644 --- a/royalnet/utils/formatters.py +++ b/royalnet/utils/formatters.py @@ -2,16 +2,29 @@ import typing import re -def andformat(l: typing.List[str], middle=", ", final=" and ") -> str: - """Convert a :py:class:`list` to a :py:class:`str` by adding ``final`` between the last two elements and ``middle`` between the others. +def andformat(l: typing.Collection[str], middle=", ", final=" and ") -> str: + """Convert a iterable (such as a :class:`list`) to a :class:`str` by adding ``final`` between the last two elements and ``middle`` between the others. - Parameters: - l: the input :py:class:`list`. - middle: the :py:class:`str` to be added between the middle elements. - final: the :py:class:`str` to be added between the last two elements. + Args: + l: the input iterable. + middle: the :class:`str` to be added between the middle elements. + final: the :class:`str` to be added between the last two elements. Returns: - The resulting :py:class:`str`.""" + The resulting :py:class:`str`. + + Examples: + :: + + >>> andformat(["Steffo", "Kappa", "Proto"]) + "Steffo, Kappa and Proto" + + >>> andformat(["Viktya", "Sensei", "Cate"], final=" e ") + "Viktya, Sensei e Cate" + + >>> andformat(["Paltri", "Spaggia", "Gesù", "Mallllco"], middle="+", final="+") + "Paltri+Spaggia+Gesù+Mallllco" + """ result = "" for index, item in enumerate(l): result += item @@ -22,48 +35,65 @@ def andformat(l: typing.List[str], middle=", ", final=" and ") -> str: return result -def plusformat(i: int, empty_if_zero: bool = False) -> str: - """Convert an :py:class:`int` to a :py:class:`str`, prepending a ``+`` if it's greater than 0. +def underscorize(string: str) -> str: + """Replace all non-word characters in a :class:`str` with underscores. - Parameters: - i: the :py:class:`int` to convert. - empty_if_zero: Return an empty string if ``i`` is zero. - - Returns: - The resulting :py:class:`str`.""" - if i == 0 and empty_if_zero: - return "" - if i > 0: - return f"+{i}" - return str(i) - - -def fileformat(string: str) -> str: - """Ensure a string can be used as a filename by replacing all non-word characters with underscores. + It is particularly useful when you want to use random strings from the Internet as filenames. Parameters: string: the input string. Returns: - A valid filename string.""" + The resulting string. + + Example: + :: + + >>> underscorize("LE EPIC PRANK [GONE WRONG!?!?]") + "LE EPIC PRANK _GONE WRONG_____" + + """ return re.sub(r"\W", "_", string) def ytdldateformat(string: typing.Optional[str], separator: str = "-") -> str: - """Convert the weird date string returned by ``youtube-dl`` into the ``YYYY-MM-DD`` format. + """Convert the date :class:`str` returned by `youtube_dl `_ into + the ``YYYY-MM-DD`` format. Parameters: - string: the input string, in the ``YYYYMMDD`` format. - separator: the string to add between the years, the months and the days. Defaults to ``-``. + string: the input :class:`str`, in the ``YYYYMMDD`` format used by youtube_dl. + separator: the :class:`str` to add between the years, the months and the days. Defaults to ``"-"``. Returns: - The resulting string, in the format ``YYYY-MM-DD`` format.""" + The resulting :class:`str` in the new format. + + Example: + :: + + >>> ytdldateformat("20111111") + "2011-11-11" + + >>> ytdldateformat("20200202", separator=".") + "2020.02.02" + + """ if string is None: return "" return f"{string[0:4]}{separator}{string[4:6]}{separator}{string[6:8]}" def numberemojiformat(l: typing.List[str]) -> str: + """Convert a :class:`list` to a Unicode string with one item on every line numbered with emojis. + + Parameters: + l: the list to convert. + + Returns: + The resulting Unicode string. + + Examples: + Cannot be displayed, as Sphinx does not render emojis properly. + """ number_emojis = ["1ï¸âƒ£", "2ï¸âƒ£", "3ï¸âƒ£", "4ï¸âƒ£", "5ï¸âƒ£", "6ï¸âƒ£", "7ï¸âƒ£", "8ï¸âƒ£", "9ï¸âƒ£", "🔟"] extra_emoji = "*ï¸âƒ£" result = "" @@ -75,15 +105,31 @@ def numberemojiformat(l: typing.List[str]) -> str: return result -def splitstring(s: str, max: int) -> typing.List[str]: - l = [] - while s: - l.append(s[:max]) - s = s[max:] - return l +def ordinalformat(number: int) -> str: + """Convert a :class:`int` to the corresponding English ordinal :class:`str`. + Parameters: + number: the number to convert. -def ordinalformat(number: int): + Returns: + The corresponding English `ordinal numeral `_. + + Examples: + :: + + >>> ordinalformat(1) + "1st" + >>> ordinalformat(2) + "2nd" + >>> ordinalformat(11) + "11th" + >>> ordinalformat(101) + "101st" + >>> ordinalformat(112) + "112th" + >>> ordinalformat(0) + "0th" + """ if 10 <= number % 100 < 20: return f"{number}th" if number % 10 == 1: diff --git a/royalnet/utils/log.py b/royalnet/utils/log.py new file mode 100644 index 00000000..edc7e21c --- /dev/null +++ b/royalnet/utils/log.py @@ -0,0 +1,23 @@ +from typing import * +import logging + +try: + import coloredlogs +except ImportError: + coloredlogs = None + + +log_format = "{asctime}\t| {processName}\t| {name}\t| {message}" + + +def init_logging(logging_cfg: Dict[str, Any]): + royalnet_log: logging.Logger = logging.getLogger("royalnet") + royalnet_log.setLevel(logging_cfg["log_level"]) + stream_handler = logging.StreamHandler() + if coloredlogs is not None: + stream_handler.formatter = coloredlogs.ColoredFormatter(log_format, style="{") + else: + stream_handler.formatter = logging.Formatter(log_format, style="{") + if len(royalnet_log.handlers) < 1: + royalnet_log.addHandler(stream_handler) + royalnet_log.debug("Logging: ready") diff --git a/royalnet/utils/multilock.py b/royalnet/utils/multilock.py new file mode 100644 index 00000000..0861a2fb --- /dev/null +++ b/royalnet/utils/multilock.py @@ -0,0 +1,58 @@ +from asyncio import Event +from contextlib import asynccontextmanager +import logging + + +log = logging.getLogger(__name__) + + +class MultiLock: + """A lock that can allow both simultaneous access and exclusive access to a resource.""" + def __init__(self): + self._counter: int = 0 + self._normal_event: Event = Event() + self._exclusive_event: Event = Event() + self._normal_event.set() + self._exclusive_event.set() + + def _check_event(self): + if self._counter > 0: + self._normal_event.clear() + else: + self._normal_event.set() + + @asynccontextmanager + async def normal(self): + """Acquire the lock for simultaneous access.""" + log.debug(f"Waiting for exclusive lock end: {self}") + await self._exclusive_event.wait() + log.debug(f"Acquiring normal lock: {self}") + self._counter += 1 + self._check_event() + try: + yield + finally: + log.debug(f"Releasing normal lock: {self}") + self._counter -= 1 + self._check_event() + + @asynccontextmanager + async def exclusive(self): + """Acquire the lock for exclusive access.""" + log.debug(f"Waiting for exclusive lock end: {self}") + # TODO: check if this actually works + await self._exclusive_event.wait() + self._exclusive_event.clear() + log.debug(f"Waiting for normal lock end: {self}") + await self._normal_event.wait() + try: + log.debug("Acquiring exclusive lock: {self}") + self._exclusive_event.clear() + yield + finally: + log.debug("Releasing exclusive lock: {self}") + self._exclusive_event.set() + + def __repr__(self): + return f"" diff --git a/royalnet/utils/sentry.py b/royalnet/utils/sentry.py new file mode 100644 index 00000000..f854dc06 --- /dev/null +++ b/royalnet/utils/sentry.py @@ -0,0 +1,47 @@ +import logging +import sys +import traceback +from typing import * +from royalnet.version import semantic + +try: + import sentry_sdk + from sentry_sdk.integrations.aiohttp import AioHttpIntegration + from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration + from sentry_sdk.integrations.logging import LoggingIntegration +except ImportError: + sentry_sdk = None + AioHttpIntegration = None + SqlalchemyIntegration = None + LoggingIntegration = None + + +log = logging.getLogger(__name__) + + +def init_sentry(sentry_cfg: Dict[str, Any]): + if sentry_sdk is None: + raise ImportError("`sentry` extra is not installed") + log.debug("Initializing Sentry...") + release = f"royalnet@{semantic}" + sentry_sdk.init(sentry_cfg["dsn"], + integrations=[AioHttpIntegration(), + SqlalchemyIntegration(), + LoggingIntegration(event_level=None)], + release=release) + log.info(f"Sentry: {release}") + + +# noinspection PyUnreachableCode +def sentry_exc(exc: Exception, + level: str = "ERROR"): + if sentry_sdk is not None: + with sentry_sdk.configure_scope() as scope: + scope.set_level(level.lower()) + sentry_sdk.capture_exception(exc) + level_int: int = logging._nameToLevel[level.upper()] + log.log(level_int, f"Captured {level.capitalize()}: {exc}") + # If started in debug mode (without -O), raise the exception, allowing you to see its source + if __debug__: + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, exc_traceback) diff --git a/royalnet/utils/sleepuntil.py b/royalnet/utils/sleep_until.py similarity index 83% rename from royalnet/utils/sleepuntil.py rename to royalnet/utils/sleep_until.py index 63d08ed0..3b16fcc5 100644 --- a/royalnet/utils/sleepuntil.py +++ b/royalnet/utils/sleep_until.py @@ -3,7 +3,7 @@ import datetime async def sleep_until(dt: datetime.datetime) -> None: - """Block the call until the specified datetime. + """Sleep until the specified datetime. Warning: Accurate only to seconds.""" diff --git a/royalnet/utils/wikirender.py b/royalnet/utils/wikirender.py deleted file mode 100644 index 32e385c9..00000000 --- a/royalnet/utils/wikirender.py +++ /dev/null @@ -1,29 +0,0 @@ -import re -import markdown2 - - -class RenderError(Exception): - """An error occurred while trying to render the page.""" - - -def prepare_page_markdown(markdown): - if list(markdown).count(">") > 99: - raise RenderError("Too many nested quotes") - converted_md = markdown2.markdown(markdown.replace("<", "<"), - extras=["spoiler", "tables", "smarty-pants", "fenced-code-blocks"]) - converted_md = re.sub(r"{https?://(?:www\.)?(?:youtube\.com/watch\?.*?&?v=|youtu.be/)([0-9A-Za-z-]+).*?}", - r'
' - r' ' - r'
', converted_md) - converted_md = re.sub(r"{https?://clyp.it/([a-z0-9]+)}", - r'
' - r' ' - r'
', converted_md) - return converted_md diff --git a/royalnet/version.py b/royalnet/version.py index 37d8d6a0..8085a26e 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1,4 +1 @@ -semantic = "5.0a93" - -if __name__ == "__main__": - print(semantic) +semantic = "5.1a1" diff --git a/royalnet/web/constellation.py b/royalnet/web/constellation.py deleted file mode 100644 index e9de9dbd..00000000 --- a/royalnet/web/constellation.py +++ /dev/null @@ -1,95 +0,0 @@ -import typing -import uvicorn -import logging -import sentry_sdk -from sentry_sdk.integrations.aiohttp import AioHttpIntegration -from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration -from sentry_sdk.integrations.logging import LoggingIntegration -import royalnet -import keyring -from starlette.applications import Starlette -from .star import PageStar, ExceptionStar - - -log = logging.getLogger(__name__) - - -class Constellation: - def __init__(self, - secrets_name: str, - database_uri: str, - tables: set, - page_stars: typing.List[typing.Type[PageStar]] = None, - exc_stars: typing.List[typing.Type[ExceptionStar]] = None, - *, - debug: bool = __debug__,): - if page_stars is None: - page_stars = [] - - if exc_stars is None: - exc_stars = [] - - self.secrets_name: str = secrets_name - - log.info("Creating starlette app...") - self.starlette = Starlette(debug=debug) - - log.info(f"Creating alchemy with tables: {' '.join([table.__name__ for table in tables])}") - self.alchemy: royalnet.database.Alchemy = royalnet.database.Alchemy(database_uri=database_uri, tables=tables) - - log.info("Registering page_stars...") - for SelectedPageStar in page_stars: - try: - page_star_instance = SelectedPageStar(constellation=self) - except Exception as e: - log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}") - sentry_sdk.capture_exception(e) - continue - log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}") - self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods) - - log.info("Registering exc_stars...") - for SelectedExcStar in exc_stars: - try: - exc_star_instance = SelectedExcStar(constellation=self) - except Exception as e: - log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}") - sentry_sdk.capture_exception(e) - continue - log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}") - self.starlette.add_exception_handler(exc_star_instance.error, exc_star_instance.page) - - def _init_sentry(self): - sentry_dsn = self.get_secret("sentry") - if sentry_dsn: - # noinspection PyUnreachableCode - if __debug__: - release = "DEV" - else: - release = royalnet.version.semantic - log.info(f"Sentry: enabled (Royalnet {release})") - self.sentry = sentry_sdk.init(sentry_dsn, - integrations=[AioHttpIntegration(), - SqlalchemyIntegration(), - LoggingIntegration(event_level=None)], - release=release) - else: - log.info("Sentry: disabled") - - def get_secret(self, username: str): - return keyring.get_password(f"Royalnet/{self.secrets_name}", username) - - def set_secret(self, username: str, password: str): - return keyring.set_password(f"Royalnet/{self.secrets_name}", username, password) - - def run_blocking(self, address: str, port: int, verbose: bool): - if verbose: - core_logger = logging.root - core_logger.setLevel(logging.DEBUG) - stream_handler = logging.StreamHandler() - stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") - core_logger.addHandler(stream_handler) - core_logger.debug("Logging setup complete.") - self._init_sentry() - log.info(f"Running constellation server on {address}:{port}...") - uvicorn.run(self.starlette, host=address, port=port) diff --git a/royalnet/web/error.py b/royalnet/web/error.py deleted file mode 100644 index 239e34ee..00000000 --- a/royalnet/web/error.py +++ /dev/null @@ -1,7 +0,0 @@ -from starlette.responses import JSONResponse - - -def error(code: int, description: str) -> JSONResponse: - return JSONResponse({ - "error": description - }, status_code=code) diff --git a/royalnet/web/star.py b/royalnet/web/star.py deleted file mode 100644 index bc8c5d24..00000000 --- a/royalnet/web/star.py +++ /dev/null @@ -1,37 +0,0 @@ -import typing -from starlette.requests import Request -from starlette.responses import Response -if typing.TYPE_CHECKING: - from .constellation import Constellation - - -class Star: - tables: set = {} - - def __init__(self, constellation: "Constellation"): - self.constellation: "Constellation" = constellation - - async def page(self, request: Request) -> Response: - raise NotImplementedError() - - @property - def alchemy(self): - return self.constellation.alchemy - - @property - def Session(self): - return self.constellation.alchemy.Session - - @property - def session_acm(self): - return self.constellation.alchemy.session_acm - - -class PageStar(Star): - path: str = NotImplemented - - methods: typing.List[str] = ["GET"] - - -class ExceptionStar(Star): - error: typing.Union[typing.Type[Exception], int] diff --git a/sample_config.toml b/sample_config.toml new file mode 100644 index 00000000..fc2feb99 --- /dev/null +++ b/sample_config.toml @@ -0,0 +1,112 @@ +# ROYALNET CONFIGURATION FILE + +[Herald] +# Enable the herald module, allowing different parts of Royalnet to talk to each other +# Requires the `herald` extra to be installed +enabled = true + +[Herald.Local] +# Run locally a Herald web server (websocket) that other parts of Royalnet can connect to +enabled = true +# The address of the network interface on which the Herald server should listen for connections +# If 0.0.0.0, listen for connections on all interfaces +# If 127.0.0.1, listen only for connections coming from the local machine +address = "0.0.0.0" +# The port on which the Herald server should run +port = 44444 +# A password required to connect to the local Herald server +secret = "CHANGE-ME" +# Use HTTPS instead of HTTP for Herald connections +secure = false # Not supported yet! +# Use a different HTTP path for Herald connections +path = "/" # Different values aren't supported yet + +[Herald.Remote] +# Connect to a remote Herald web server (websocket) +# Requires the `herald` extra to be installed +enabled = false +# The address of the remote Herald server +address = "0.0.0.0" +# The port of the remote Herald server +port = 44444 +# The password required to connect to the remote Herald server +secret = "CHANGE-ME" +# Use HTTPS instead of HTTP for Herald connections +secure = false # Not supported yet! +# Use a different HTTP path for Herald connections +path = "/" # Different values aren't supported yet + + +[Alchemy] +# Use the Alchemy module of Royalnet to connect to a PostgreSQL server +# Requires either the `alchemy_easy` or the `alchemy_hard` extras to be installed +enabled = true +# The URL of the database you want to connect to, in sqlalchemy format: +# https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls +database_url = "postgresql://username:password@host:port/database" + +[Constellation] +# Run locally a Constellation web server (uvicorn+starlette) serving the Stars contained in the enabled Packs +# Requires the `constellation` extra to be installed +enabled = true +# The address of the network interface on which the Constellation should listen for requests +# If 0.0.0.0, listen for requests on all interfaces +# If 127.0.0.1, listen only for requests coming from the local machine +address = "0.0.0.0" +# The port on which the Constellation should run +port = 44445 + +[Serfs] + +[Serfs.Telegram] +# Use the Telegram Serf (python-telegram-bot) included in Royalnet +# Requires the `telegram` extra to be installed +enabled = true +# The Bot API Token of the bot you want to use for Royalnet +# Obtain one at https://t.me/BotFather +token = "0000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +# The size of the Connection Pool used by python-telegram-bot +# 8 should be fine, but if you start getting `TimeoutError: QueuePool limit of size X overflow Y reached" errors, +# increasing this number should fix them +pool_size = 8 +# The maximum amount of time to wait for a response from Telegram before raising a `TimeoutError` +# It also is the time that python-telegram-bot will wait before sending a new request if no updates are being received. +read_timeout = 60 + +[Serfs.Discord] +# Use the Discord Serf (discord.py) included in Royalnet +# Requires the `discord` extra to be installed +enabled = true +# The Discord Bot Token of the bot you want to use for Royalnet +# Obtain one at https://discordapp.com/developers/applications/ > Bot > Token +token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + +[Logging] +# Print to stderr all logging events of an equal or greater level than this +# Possible values are "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" +log_level = "INFO" +# Optional: install the `coloredlogs` extra for colored output! + +[Sentry] +# Connect Royalnet to a https://sentry.io/ project for error logging +# Requires the `sentry` extra to be installed +enabled = false +# Get one at https://sentry.io/settings/YOUR-ORG/projects/YOUR-PROJECT/keys/ +dsn = "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/1111111" + +[Packs] +# The Python package name of the Packs you want to be usable in Royalnet +# Please note that the `royalnet.backpack` Pack should always be available! +active = [ + "royalnet.backpack", # DO NOT REMOVE THIS OR THINGS WILL BREAK + # "yourpack", + +] + +# Configuration settings for specific packs +[Packs."royalnet.backpack"] +# Enable exception debug commands and stars +exc_debug = false + +# Add your packs config here! +# [Packs."yourpack"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 5c45fba9..00000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -import royalnet.version -import setuptools - -with open("README.md", "r") as f: - long_description = f.read() - -with open("requirements.txt", "r") as f: - install_requires = [line for line in f.readlines() if not line.startswith("#")] - -setuptools.setup( - name="royalnet", - version=royalnet.version.semantic, - author="Stefano Pigozzi", - author_email="ste.pigozzi@gmail.com", - description="The great bot network of the User Games community", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/royal-games/royalnet", - packages=setuptools.find_packages(), - install_requires=install_requires, - python_requires=">=3.7", - classifiers=[ - "Development Status :: 3 - Alpha", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.7", - "Topic :: Internet", - "Topic :: Database", - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Multimedia :: Video", - "License :: OSI Approved :: MIT License" - ], - dependency_links=["https://github.com/Rapptz/discord.py/tarball/master"], - include_package_data=True, - zip_safe=False -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/to_pypi.bat b/to_pypi.bat deleted file mode 100644 index 40941be4..00000000 --- a/to_pypi.bat +++ /dev/null @@ -1,4 +0,0 @@ - -del /f /q /s dist\*.* -python setup.py sdist bdist_wheel -twine upload dist/* diff --git a/to_pypi.sh b/to_pypi.sh deleted file mode 100755 index 7a431000..00000000 --- a/to_pypi.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Royalnet must be installed with `develop` -VERSION=$(python3.7 -m royalnet.version) - -rm -rf dist -python setup.py sdist bdist_wheel -twine upload "dist/royalnet-$VERSION"* -git add * -git commit -m "$VERSION" -git push -hub release create --message "Royalnet $VERSION" --prerelease "$VERSION"