From a14ab0a7601ff5c197fe43d42410d8ed6bfd26a8 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Sat, 13 Nov 2021 20:33:57 +0100 Subject: initial commit --- LICENSE | 661 ++++++++++++++++++++++ README.md | 62 +++ debian/changelog | 5 + debian/compat | 1 + debian/control | 21 + debian/copyright | 6 + debian/rules | 16 + debian/source/format | 1 + example_content/hello/bye.js | 7 + example_content/hello/cc0.txt | 121 ++++ example_content/hello/hello.js | 7 + example_content/hello/index.json | 299 ++++++++++ example_content/hello/message.js | 8 + pytest.ini | 12 + setup.py | 76 +++ src/conftest.py | 0 src/pydrilla/__init__.py | 1 + src/pydrilla/config.json | 13 + src/pydrilla/development_config.json | 24 + src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po | 127 +++++ src/pydrilla/pydrilla.py | 700 ++++++++++++++++++++++++ src/pydrilla/templates/base.html | 94 ++++ src/pydrilla/templates/index.html | 32 ++ src/pydrilla_dev_helper.py | 293 ++++++++++ src/test/__init__.py | 0 src/test/test_pydrilla.py | 90 +++ 26 files changed, 2677 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 example_content/hello/bye.js create mode 100644 example_content/hello/cc0.txt create mode 100644 example_content/hello/hello.js create mode 100644 example_content/hello/index.json create mode 100644 example_content/hello/message.js create mode 100644 pytest.ini create mode 100755 setup.py create mode 100644 src/conftest.py create mode 100644 src/pydrilla/__init__.py create mode 100644 src/pydrilla/config.json create mode 100644 src/pydrilla/development_config.json create mode 100644 src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po create mode 100644 src/pydrilla/pydrilla.py create mode 100644 src/pydrilla/templates/base.html create mode 100644 src/pydrilla/templates/index.html create mode 100644 src/pydrilla_dev_helper.py create mode 100644 src/test/__init__.py create mode 100644 src/test/test_pydrilla.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + 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/README.md b/README.md new file mode 100644 index 0000000..37d18ee --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ + +[//]: # ( SPDX-License-Identifier: CC0-1.0 ) + +[//]: # ( Hydrilla's README file ) + +[//]: # ( Copyright (C) 2021 Wojtek Kosior ) + +[//]: # ( Available under the terms of Creative Commons Zero v1.0 Universal. ) + +# Hydrilla (Python implementation) + +This is the repository of Python incarnation of [Hydrilla](https://hydrillabugs.koszko.org/projects/hydrilla/wiki/Wiki), a repository software to serve [Haketilo](https://hydrillabugs.koszko.org/projects/haketilo/wiki) packages. + +## Dependencies + +* flask +* pytest (for running tests) + +Additionally, Hydrilla requires Python 3. + +## Building + +We're supplying debian packaging. To generate the necessary `debian.tar.gz` and `orig.tar.gz` files, run: +``` shell +python3 setup.py make_tarballs +``` + +This will generate appropriate `debian.tar.gz` and `orig.tar.gz` in the parent of the project directory. + +*TODO: describe debian procedure...* + +We're using setuptools. This means you can also do something like: +``` shell +python3 setup.py install +``` + +*TODO: describe setuptools considerations and mention avoiding pip...* + +## Running +Hydrilla can be run from source, without prior installation, provided that its dependencies are present on the system. For this to work, you should first generate GNU Gettext `.mo` files. This is most easily achieved using our custom `setup.py` command: +``` shell +python3 setup.py msgfmt +``` + +A simple session can then be started with: +``` shell +python3 setup.py run +``` + +*TODO...* + +## Copying + +Hydrilla is entirely available under the GNU Affero General Public License version 3 or later. Some files might also give you broader permissions, see comments inside them. + +*I, Wojtek Kosior, thereby promise not to sue for violation of this project's license. Although I request that you do not make use this code in a proprietary program, I am not going to enforce this in court.* + +## Contributing + +Please visit our Redmine instance at https://hydrillabugs.koszko.org. + +You can also write an email to koszko@koszko.org. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..71df1a2 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +pydrilla (0.2-1) UNRELEASED; urgency=medium + + * Initial release. + + -- Wojtek Kosior Fri, 05 Nov 2021 15:39:43 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..9a03714 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..9ba04f3 --- /dev/null +++ b/debian/control @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: CC0-1.0 + +# Hydrilla's debian/control file +# +# Copyright (C) 2021 Wojtek Kosior +# +# Available under the terms of Creative Commons Zero v1.0 Universal. + +Source: pydrilla +Maintainer: Wojtek Kosior +Section: web +Priority: optional +Standards-Version: 4.3.0 +Build-Depends: debhelper (>=11~), dh-python, python3-all, python3-setuptools, python3-flask, python3-pytest + +Package: python3-pydrilla +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: serve Hachette packages + Hydrilla (Pydrilla) implements repository server for independent website + resources. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..7e445e5 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,6 @@ +Pydrilla is copyright 2021 Wojtek Kosior. +Pydrilla can be used under the terms of GNU Affero General Public License as +published by the Free Software Foundation. + +Additionally, Wojtek Kosior promises not to actually sue for violations of the +license. \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..a0880f7 --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#! /usr/bin/make -f + +# SPDX-License-Identifier: CC0-1.0 + +# Hydrilla's debian/rules file +# +# Copyright (C) 2021 Wojtek Kosior +# +# Available under the terms of Creative Commons Zero v1.0 Universal. + +export DH_VERBOSE = 1 +export PYBUILD_NAME = pydrilla +export PYBUILD_TEST_PYTEST = 1 + +%: + dh $@ --with python3 --buildsystem=pybuild diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..46ebe02 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) \ No newline at end of file diff --git a/example_content/hello/bye.js b/example_content/hello/bye.js new file mode 100644 index 0000000..e6fd70c --- /dev/null +++ b/example_content/hello/bye.js @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Copyright (C) 2021 Wojtek Kosior +// +// Available under the terms of Creative Commons Zero v1.0 Universal. + +console.log(bye_message + "apple!"); diff --git a/example_content/hello/cc0.txt b/example_content/hello/cc0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/example_content/hello/cc0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/example_content/hello/hello.js b/example_content/hello/hello.js new file mode 100644 index 0000000..d87ea7f --- /dev/null +++ b/example_content/hello/hello.js @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Copyright (C) 2021 Wojtek Kosior +// +// Available under the terms of Creative Commons Zero v1.0 Universal. + +console.log(hello_message + "apple!"); diff --git a/example_content/hello/index.json b/example_content/hello/index.json new file mode 100644 index 0000000..34f5eba --- /dev/null +++ b/example_content/hello/index.json @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Copyright (C) 2021 Wojtek Kosior +// Available under the terms of Creative Commons Zero v1.0 Universal. + +// This is an example index.json file describing Hydrilla site content. As you +// can see, for storing site content information Hydrilla utilizes JSON with an +// additional extension in the form of '//' comments support. Hydrilla shall +// look into each direct subdirectory of the content directory passed to it +// (via a cofig file option). If such subsirectory contains an index.json file, +// Hydrilla shall process it. + +// An index.json file conveys definitions of site resources, pattern->payload +// mappings and licenses thereof. The definitions may reference files under +// index.json's containing directory, using relative paths. This is how scripts, +// license texts, etc. are included. Unix paths (using '/' as separator) are +// assumed. It is not allowed for an index.json file to reference files outside +// its directory. + +// Certain objects are allowed to contain a "comment" field. Although '//' +// comments can be used in index.json files, they will be stripped when the file +// is processed. If a comment should be included in the JSON definitions served +// by Hydrilla API, it should be put in a "comment" field of the proper object. + +// Various kinds of objects contain version information. Version is always an +// array of integers, with major version number being the first array item. When +// applicable, a version is accompanied by a revision field which contains a +// positive integer. If versions specified by arrays of different length need to +// be compared, the shorter array gets padded with zeroes on the right. This +// means that for example version 1.3 could be given as both [1, 3] and +// [1, 3, 0, 0] (aka 1.3.0.0) and either would mean the same. + +{ + // Once our json schema changes, this version will change. Our software will + // be able to handle both current and older formats thanks to this + // information present in every index.json file. Different schema versions + // are always incompatible (e.g. a Hydrilla instance that understands schema + // version 0.2.0.0 will not understand version 0.2.0.1). Schemas that are + // backwards-compatible will be denoted by a different revision. + // We will try to make schema version match the version of Hydrilla software + // that introduced it. + "schema_version": [0, 2], + "schema_revision": 1, + + // Copyright of this json file. It's a list of copyright holder information + // objects. Alternatively, "auto" can be used to make Hydrilla attempt to + // extract copyright info from the comment at the beginning of the file. + "copyright": [ + // There can be multiple entries, one for each co-holder of the + // copyright. + { + // There can also be multiple years, like ["2021","2023-2024"]. + "years": ["2021"], + // Name of the copyright holder. Depending on the situation it can + // be just the first name, name+surname, a company name, a + // pseudonym, etc. + "holder": "Wojtek Kosior" + } + ], + + // License of this json file. Identifier has to be known to Hydrilla. Can + // be defined either in the same or another index.json file as a "license" + // item. It is possible to specify license combinations, like: + // [["Expat", "and", "Apache-2.0"], "or", "GPL-3.0-only"] + // Alternatively, "auto" can be used to make Hydrilla attempt to extract + // copyright info from this file's SPDX license identifier. + "licenses": "CC0-1.0", + + // Where this software/work initially comes from. In some cases (i.e. when + // the developer of content is also the one who packages it for Hydrilla) + // this might be the same as "package_url". + "upstream_url": "https://git.koszko.org/pydrilla/tree/example_content/hello", + + // Where sources for the packaging of this content can be found. + "package_url": "https://git.koszko.org/pydrilla/tree/example_content/hello", + + // Additional "comment" field can be used if needed. + // "comment": "" + + // List of actual site resources, pattern->payload mappings and licenses. + // Each of them is represented by an object. Meta-sites and replacement site + // interfaces will also belong here once they get implemented. + "definitions": [ + { + // Value of "type" can currently be one of: "resource", "license" + // and "mapping". The one we have here, "resource", defines a list + // of injectable scripts that can be used as a payload or as a + // dependency of another "resource". In the future CSS style sheets + // and WASM modules will also be composite parts of a "resource" as + // scripts are now. + "type": "resource", + + // Used when referring to this resource in "dependencies" list of + // another resource or in "payload" field of a mapping. Should + // be consize and can only use a restricted set of characters. It + // has to match: [-0-9a-zA-Z] + "identifier": "helloapple", + + // "long_name" should be used to specify a user-friendly alternative + // to an identifier. It should generally not collide with a long + // name of some resource with a different uuid and also shouldn't + // change in-between versions of the same resource, although + // exceptions to both rules might be considered. Long name is + // allowed to contain arbitrary unicode characters (within reason!). + "long_name": "Hello Apple", + + // Different versions (e.g. 1.0 and 1.3) of the same resource can be + // defined in separate index.json files. This makes it easy to + // accidently cause an identifier clash. To help detect it, we + // require that each resource has a uuid associated with it. Attempt + // to define multiple resources with the same identifier and + // different uuids will result in an error being reported. Defining + // multiple resources with different identifiers and the same uuid + // is disallowed for now (it may be later permitted if we consider + // it good for some use-case). + "uuid": "a6754dcb-58d8-4b7a-a245-24fd7ad4cd68", + + // Version should match the upstream version of the resource (e.g. a + // version of javascript library). Revision number starts as 1 for + // each new resource version and gets incremented by 1 each time a + // modification to the packaging of this version is done. Hydrilla + // will allow multiple definitions of the same resource to load, as + // long as their versions differ. Thanks to the "version" and + // "revision" fields, clients will know they have to update certain + // resource after it has been updated. If multiple definitions of + // the same version of given resource are provided, an error is + // generated (even if those definitions differ by revision number). + "version": [2021, 11, 10], + "revision": 1, + + // A short, meaningful description of what the resource is and/or + // what it does. + "description": "greets an apple", + + // If needed, a "comment" field can be added to provide some + // additional information. + // "comment": "this resource something something", + + // One should specify the copyright and licensing terms of the + // entire package. The format is the same as when specifying these + // for the index.json file, except "auto" cannot be used. + "copyright": [{"years": ["2021"], "holder": "Wojtek Kosior"}], + "licenses": "CC0-1.0", + + // Resource's "dependencies" array shall contain names of other + // resources that (in case of scripts at least) should get evaluated + // on a page before this resource's own scripts. + "dependencies": ["hello-message"], + + // Array of javascript files that belong to this resource. + "scripts": [ + { + // Script name. It should also be a valid file path. + "name": "hello.js", + // Copyright and license info of a script file can be + // specified using the same format as in the case of the + // index.json file itself. If "copyright" or "license" is + // not provided, Hydrilla assumes it to be the same as the + // value specified for the resource itself. + "copyright": "auto", + "licenses": "auto" + }, { + "name": "bye.js" + } + ] + }, { + "type": "resource", + "identifier": "hello-message", + "long_name": "Hello Message", + "uuid": "1ec36229-298c-4b35-8105-c4f2e1b9811e", + "version": [2021, 11, 10], + "revision": 2, + "description": "define messages for saying hello and bye", + "copyright": [{"years": ["2021"], "holder": "Wojtek Kosior"}], + "licenses": "CC0-1.0", + // If "dependencies" is empty, it can also be omitted. + // "dependencies": [], + "scripts": [{"name": "message.js"}] + }, { + "type": "mapping", + + // Has similar function to resource's identifier. Should be consize + // and can only use a restricted set of characters. It has to match: + // [-0-9a-zA-Z] + // It can be the same as some resource identifier (those are + // different entities and are treated separately). + "identifier": "helloapple", + + // "long name" and "uuid" have the same meaning as in the case of + // resources. Uuids of a resource and a mapping can technically be + // the same, but it is recommended to avoid even this kind of + // repetition. + "long_name": "Hello Apple", + "uuid": "54d23bba-472e-42f5-9194-eaa24c0e3ee7", + + // "version" differs from its counterpart in resource in that it has + // no accompanying revision number. + "version": [2021, 11, 10], + + // A short, meaningful description of what the mapping does. + "description": "causes apple to get greeted on Hydrillabugs issue tracker", + + // A comment, if necessary. + // "comment": "blah blah because bleh" + + // The "payloads" array specifies, which payloads are to be + // applied to which URLs. + "payloads": [ + { + // Should be a valid Haketilo URL pattern. + "pattern": "https://hydrillabugs.koszko.org/***", + // Should be the name of an existing resource. The resource + // may, but doesn't have to, be defined in the same + // index.json file. + "payload": "helloapple" + }, + // More associations may follow. + { + "pattern": "https://hachettebugs.koszko.org/***", + "payload": "helloapple" + } + ] + }, { + "type": "license", + + // Will be used to refer to this license in other places. Should + // match the SPDX identifier if possible (despite that, please use + // "Expat" instead of "MIT" where possible). Unlike other definition + // types, "license" does not allow uuids to be used to avoid license + // id clashes. Any attempt to define multiple licenses with the same + // id will result in an error being reported. + "identifier": "CC0-1.0", + + // This long name must also be unique among all license definitions. + "long_name": "Creative Commons Zero v1.0 Universal", + + // We don't use "version" in license definitions. We do, however, + // use "revision" to indicate changes to the packaging of a license. + // Revision should be increased by 1 at each such change. + "revision": 2, + + "legal_text": [ + // Legal text can be available in multiple forms. Usually just + // plain .txt file is enough, though. + { + // "format" should match an agreed-upon MIME type if + // possible. + "format": "text/plain", + // Value of "file" should be a path relative to the + // directory of index.json file. + "file": "cc0.txt" + } + // If a markdown version of CC0 was provided, we could add this: + // { + // "format": "text/markdown", + // "file": "cc0.md" + // } + ] + + // If needed, a "comment" field can be added to clarify something. + // For example, when definind "Expat" license we could add: + // + // "comment": "Expat license is the most common form of the license often called \"MIT\". Many other forms of \"MIT\" license exist. Here the name \"Expat\" is used to avoid ambiguity." + + // If applicable, a "notice" can be included. It shall then be a + // path (relative to index.json) to a plain text file with that + // notice. + // + // "notice": "license-notice.txt" + // + // This is needed for example in case of GNU licenses (both with and + // without exceptions). For example, + // "GPL-3.0-or-later-with-html-exception" could have the following + // in its notice file: + // + // This program is free software: you can redistribute it and/or + // modify it under the terms of the GNU General Public License as + // published by the Free Software Foundation, either version 3 of + // the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, + // but WITHOUT ANY WARRANTY; without even the implied warranty of + // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + // GNU General Public License for more details. + // + // As a special exception to the GPL, any HTML file which merely + // makes function calls to this code, and for that purpose + // includes it by reference shall be deemed a separate work for + // copyright law purposes. If you modify this code, you may extend + // this exception to your version of the code, but you are not + // obligated to do so. If you do not wish to do so, delete this + // exception statement from your version. + // + // You should have received a copy of the GNU General Public License + // along with this program. If not, see + // . + } + ] +} diff --git a/example_content/hello/message.js b/example_content/hello/message.js new file mode 100644 index 0000000..da5966d --- /dev/null +++ b/example_content/hello/message.js @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Copyright (C) 2021 Wojtek Kosior +// +// Available under the terms of Creative Commons Zero v1.0 Universal. + +var hello_message = "hello, " +var bye_message = "bye, " diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..030df26 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: CC0-1.0 + +# Disable deprecation warnings from third-party libraries +# +# Copyright (C) 2021 Wojtek Kosior +# +# Available under the terms of Creative Commons Zero v1.0 Universal. + +[pytest] +filterwarnings = + ignore::DeprecationWarning:werkzeug.*: + ignore::DeprecationWarning:jinja2.*: diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4f87ecc --- /dev/null +++ b/setup.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: CC0-1.0 + +# Setup script +# +# This file is part of Hydrilla +# +# Copyright (C) 2021 Wojtek Kosior +# +# This file is free software: you can redistribute it with or without +# modification under the terms of the CC0 1.0 Universal License as +# published by the Creative Commons Corporation. +# +# This file 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 +# CC0 1.0 Universal License for more details. + +from setuptools import setup, find_packages +import sys +import pathlib + +settings = {} +settings['version'] = '0.2' +settings['app_package_name'] = 'pydrilla' +settings['project_root'] = pathlib.Path(__file__).resolve().parent +packages_root = settings['project_root'] / 'src' +main_package_dir = packages_root / settings['app_package_name'] +settings['locales_dir'] = main_package_dir / 'locales' +settings['config_path'] = main_package_dir / 'development_config.json' + +sys.path.insert(0, str(packages_root)) +import test +import pydrilla_dev_helper + +helper = pydrilla_dev_helper.Helper(**settings) + +setup( + name=settings['app_package_name'], + version=settings['version'], + description='Hydrilla repository server (Python implementation)', +# long_description='...', +# long_description_content_type='text/plain', + url='https://hydrillabugs.koszko.org', + author='Wojtek Kosior', + author_email='koszko@koszko.org', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Environment :: Web Environment', + 'Framework :: Flask', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + 'Natural Language :: English', + 'Programming Language :: Python :: 3 :: Only' + ], + package_dir={'': 'src'}, + packages=find_packages(where='src'), + zip_safe=False, + install_requires=['flask'], + extras_require={ + 'test': ['pytest'], + }, + package_data={ + 'pydrilla': ['config.json', *helper.locale_files_relative()], + 'test': [] + }, + cmdclass=helper.commands() + # project_urls={ + # 'Bug Reports': 'https://', + # 'Funding': 'https://', + # 'Say Thanks!': 'http://', + # 'Source': 'https://', + # }, +) diff --git a/src/conftest.py b/src/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pydrilla/__init__.py b/src/pydrilla/__init__.py new file mode 100644 index 0000000..8d1565b --- /dev/null +++ b/src/pydrilla/__init__.py @@ -0,0 +1 @@ +from .pydrilla import create_app diff --git a/src/pydrilla/config.json b/src/pydrilla/config.json new file mode 100644 index 0000000..a6c4bf0 --- /dev/null +++ b/src/pydrilla/config.json @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Example Hydrilla config file. +// +// Copyright (C) 2021 Wojtek Kosior +// +// Available under the terms of Creative Commons Zero v1.0 Universal. + +{ + "content_dir": "/var/lib/hydrilla/content", + "static_resource_uri": "http://localhost:8000/", + "try_configs": ["/etc/pydrilla/config.json"] +} diff --git a/src/pydrilla/development_config.json b/src/pydrilla/development_config.json new file mode 100644 index 0000000..1660edb --- /dev/null +++ b/src/pydrilla/development_config.json @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Hydrilla development config file. +// +// Copyright (C) 2021 Wojtek Kosior +// +// Available under the terms of Creative Commons Zero v1.0 Universal. + +// this config is meant to be used in development environment; +// unlike config.json, it shall not be included in distribution +{ + "content_dir": "./example_content", + + // Except files from content_dir to be served there (used to redirect + // clients). + "static_resource_uri": "http://localhost:8000/", + + // Make Pydrilla error out on any warning + "werror": true + + // With the below we can make Pydrilla look for missing content items in + // another instance instead of just erroring/warning. + // ,"hydrilla_parent": "https://api.hachette-hydrilla.org/0.2/" +} diff --git a/src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po b/src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po new file mode 100644 index 0000000..f9e6a82 --- /dev/null +++ b/src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: CC0-1.0 + +# English localization +# +# This file is part of Hydrilla +# +# Copyright (C) 2021 Wojtek Kosior +# +# This file is free cultural work: you can redistribute it with or +# without modification under the terms of the CC0 1.0 Universal License +# as published by the Creative Commons Corporation. +# +# This file 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 +# CC0 1.0 Universal License for more details. + +msgid "" +msgstr "" +"Project-Id-Version: Hydrilla 0.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-11-13 19:03+0100\n" +"PO-Revision-Date: 2021-11-06 08:42+0100\n" +"Last-Translator: Wojtek Kosior \n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: pydrilla.py:97 +msgid "path_is_absolute_{}" +msgstr "Provided path '{}' is absolute." + +#: pydrilla.py:104 +#, python-brace-format +msgid "not_implemented_{what}_{where}" +msgstr "" +"Attempt to use '{what}' in '{where}' but this feature is not yet implemented." + +#: pydrilla.py:194 +#, python-brace-format +msgid "uuid_mismatch_{identifier}" +msgstr "Two different uuids were specified for item '{identifier}'." + +#: pydrilla.py:201 +#, python-brace-format +msgid "version_clash_{identifier}_{version}" +msgstr "Version '{version}' specified more than once for item '{identifier}'." + +#: pydrilla.py:297 pydrilla.py:309 +msgid "invalid_URL_{}" +msgstr "Invalid URL/pattern: '{}'." + +#: pydrilla.py:301 +msgid "disallowed_protocol_{}" +msgstr "Disallowed protocol: '{}'." + +#: pydrilla.py:391 +msgid "license_clash_{}" +msgstr "License '{}' defined more than once." + +#: pydrilla.py:408 +msgid "source_name_clash_{}" +msgstr "Source name '{}' used more than once." + +#: pydrilla.py:426 +#, python-format +msgid "couldnt_load_definition_from_%s" +msgstr "Couldn't load definition from '%s'." + +#: pydrilla.py:442 +#, python-format +msgid "no_index_license_%(source)s_%(lic)s" +msgstr "Unknown license '%(lic)s' used by index.json of '%(source)s'." + +#: pydrilla.py:449 +#, python-format +msgid "no_resource_license_%(resource)s_%(ver)s_%(lic)s" +msgstr "" +"Unknown license '%(lic)s' used by resource '%(resource)s', version '%(ver)s'." + +#: pydrilla.py:451 +#, python-format +msgid "no_mapping_license_%(mapping)s_%(ver)s_%(lic)s" +msgstr "" +"Unknown license '%(lic)s' used by mapping '%(mapping)s', version '%(ver)s'." + +#: pydrilla.py:474 +#, python-format +msgid "no_dep_%(resource)s_%(ver)s_%(dep)s" +msgstr "" +"Unknown dependency '%(dep)s' of resource '%(resource)s', version '%(ver)s'." + +#: pydrilla.py:484 +#, python-format +msgid "no_payload_%(mapping)s_%(ver)s_%(payload)s" +msgstr "" +"Unknown payload '%(payload)s' of mapping '%(mapping)s', version '%(ver)s'." + +#: pydrilla.py:512 +#, python-format +msgid "couldnt_register_%(mapping)s_%(ver)s_%(pattern)s" +msgstr "" +"Couldn't register mapping '%(mapping)s', version '%(ver)s' (pattern " +"'%(pattern)s')." + +#: pydrilla.py:566 +msgid "content_dir_path_not_dir" +msgstr "Provided \"content_dir\" path does not name a direcotry." + +#: pydrilla.py:578 +#, python-format +msgid "couldnt_load_content_from_%s" +msgstr "Couldn't load content from '%s'." + +#: pydrilla.py:603 +msgid "config_key_absent_{}" +msgstr "Config key \"{}\" not provided." + +#: templates/index.html:4 +msgid "hydrilla_welcome" +msgstr "Welcome to Hydrilla!" + +#: templates/base.html:55 templates/base.html:61 +msgid "hydrilla" +msgstr "Hydrilla" diff --git a/src/pydrilla/pydrilla.py b/src/pydrilla/pydrilla.py new file mode 100644 index 0000000..caf05a2 --- /dev/null +++ b/src/pydrilla/pydrilla.py @@ -0,0 +1,700 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Main repository logic. +# +# This file is part of Hydrilla +# +# Copyright (C) 2021 Wojtek Kosior +# +# 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 . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +from flask import Flask, Blueprint, current_app, url_for, abort, request +from jinja2 import Environment, PackageLoader +import re +#from hashlib import sha256 +import os +import pathlib +import json +import gettext +import logging + +SCHEMA_VERSION = [0, 2] + +strip_comment_re = re.compile(r''' +^ # match from the beginning of each line +( # catch the part before '//' comment + (?: # this group matches either a string or a single out-of-string character + [^"/] | + " + (?: # this group matches any in-a-string character + [^"\\] | # match any normal character + \\[^u] | # match any escaped character like '\f' or '\n' + \\u[a-fA-F0-9]{4} # match an escape + )* + " + )* +) +# expect either end-of-line or a comment: +# * unterminated strings will cause matching to fail +# * bad comment (with '/' instead of '//') will be indicated by second group +# having length 1 instead of 2 or 0 +(//?|$) +''', re.VERBOSE) + +def strip_json_comments(text): + processed = 0 + stripped_text = [] + for line in text.split('\n'): + match = strip_comment_re.match(line) + + if match is None: # unterminated string + # ignore this error, let json module report it + stripped = line + elif len(match[2]) == 1: + raise json.JSONDecodeError('bad comment', text, + processed + len(match[1])) + else: + stripped = match[1] + + stripped_text.append(stripped) + processed += len(line) + 1 + + return '\n'.join(stripped_text) + +here = pathlib.Path(__file__).resolve().parent + +bp = Blueprint('bp', __package__) + +def load_config(config_path): + config = {} + to_load = [config_path] + failures_ok = [False] + + while to_load: + path = to_load.pop() + can_fail = failures_ok.pop() + + try: + with open(config_path) as config_file: + new_config = json.loads(strip_json_comments(config_file.read())) + except Exception as e: + if can_fail: + continue + raise e from None + + config.update(new_config) + + for key, failure_ok in [('try_configs', True), ('use_configs', False)]: + paths = new_config.get(key, []) + paths.reverse() + to_load.extend(paths) + failures_ok.extend([failure_ok] * len(paths)) + + for key in ['try_configs', 'use_configs']: + if key in config: + config.pop(key) + + return config + +def get_content_file_path(path): + if os.path.sep != '/': + path.replace('/', os.path.sep) + + path = pathlib.Path(path) + if path.is_absolute(): + raise ValueError(_('path_is_absolute_{}').format(path)) + + return path + +class MyNotImplError(NotImplementedError): + '''Raised when a planned but not-yet-completed feature is used.''' + def __init__(self, what, where): + super().__init__(_('not_implemented_{what}_{where}') + .format(what=what, where=where)) + +def normalize_version(ver): + ''' + ver is an array of integers. Strip right-most zeroes from ver. + + Returns a *new* array. Doesn't modify its argument. + ''' + new_len = 0 + for i, num in enumerate(ver): + if num != 0: + new_len = i + 1 + + return ver[:new_len] + +def parse_version(ver_str): + ''' + Convert ver_str into an array representation, e.g. for ver_str="4.6.13.0" + return [4, 6, 13, 0]. + ''' + return [int(num) for num in ver_str.split('.')] + +def version_string(ver, rev=None): + ''' + ver is an array of integers. rev is an optional integer. Produce string + representation of version (optionally with revision number), like: + 1.2.3-5 + No version normalization is performed. + ''' + return '.'.join([str(n) for n in ver]) + ('' if rev is None else f'-{rev}') + +### pad_versions() and compare_versions() likely won't be needed + +# def pad_versions(ver1, ver2): +# ''' +# Each of the arguments is an array of integers. If one of the arrays is +# shorter than the other, right-pad it with zeroes to make it the same +# length as the other one. + +# Returns a tuple of *new* arrays. Doesn't modify its arguments. +# ''' +# if len(ver1) < len(ver2): +# ver2, ver1 = pad_versions(ver2, ver1) +# else: +# ver2 = [*ver2, *([0] * (len(ver1) - len(ver2)))] +# ver1 = [*ver1] + +# return ver1, ver2 + +# def compare_versions(ver1, ver2, rev1=1, rev2=1): +# ''' +# ver1 and ver2 are arrays of integers, with major version number being the +# first array item. If versions specified by arrays of different length need +# to be compared, the shorter array gets padded with zeroes on the right. +# This means that for example version 1.3 could be given as both [1, 3] and +# [1, 3, 0, 0] (aka 1.3.0.0) and either would mean the same. + +# rev1 and rev2 are revision numbers. They are appended to padded ver1 and +# ver2 arrays respectively before comparison. + +# This function returns -1, 0 or 1 when the first ver1 designates +# respectively a version lower than, equal to or greater than the one in +# ver2. +# ''' +# ver1, ver2 = pad_versions(ver1, ver2) +# ver1.append(rev1) +# ver2.append(rev2) + +# for n1, n2 in zip(ver1, ver2): +# if n1 < n2: +# return -1 +# if n1 > n2: +# return 1 + +# return 0 + +class VersionedContentItem: + '''Stores definitions of multiple versions of website content item.''' + def __init__(self): + self.uuid = None + self.identifier = None + self.by_version = {} + self.known_versions = [] + + def register_item(self, item): + if self.identifier is None: + self.identifier = item['identifier'] + self.uuid = item['uuid'] + elif self.uuid != item['uuid']: + raise ValueError(_('uuid_mismatch_{identifier}') + .format(identifier=self.identifier)) + + ver = item['version'] + ver_str = version_string(ver) + + if ver_str in self.by_version: + raise ValueError(_('version_clash_{identifier}_{version}') + .format(identifier=self.identifier, + version=ver_str)) + + self.by_version[ver_str] = item + self.known_versions.append(ver) + +class PatternTreeNode: + ''' + "Pattern Tree" is how we refer to the data structure used for querying + Haketilo patterns. Those look like 'https://*.example.com/ab/***'. The goal + is to make it possible for given URL to quickly retrieve all known patterns + that match it. + ''' + def __init__(self): + self.wildcard_matches = [None, None, None] + self.literal_match = None + self.children = {} + + def search(self, segments): + ''' + Yields all matches of this segments sequence against the tree that + starts at this node. Results are produces in order from greatest to + lowest pattern specificity. + ''' + nodes = [self] + + for segment in segments: + next_node = nodes[-1].children.get(segment) + if next_node is None: + break + + nodes.append(next_node) + + nsegments = len(segments) + cond_literal = lambda: len(nodes) == nsegments + cond_wildcard = [ + lambda: len(nodes) + 1 == nsegments and segments[-1] != '*', + lambda: len(nodes) + 1 < nsegments, + lambda: len(nodes) + 1 != nsegments or segments[-1] != '***' + ] + + while nodes: + node = nodes.pop() + + for item, condition in [(node.literal_match, cond_literal), + *zip(node.wildcard_matches, cond_wildcard)]: + if item is not None and condition(): + yield item + + def add(self, segments, item_instantiator): + ''' + Make item queryable through (this branch of) the Pattern Tree. If there + was not yet any item associated with the tree path designated by + segments, create a new one using item_instantiator() function. Return + all items matching this path (both the ones that existed and the ones + just created). + ''' + node = self + + for i, segment in enumerate(segments): + wildcards = node.wildcard_matches + + child = node.children.get(segment) or PatternTreeNode() + node.children[segment] = child + node = child + + if node.literal_match is None: + node.literal_match = item_instantiator() + + if segment not in ('*', '**', '***'): + return [node.literal_match] + + if wildcards[len(segment) - 1] is None: + wildcards[len(segment) - 1] = item_instantiator() + + return [node.literal_match, wildcards[len(segment) - 1]] + +proto_regex = re.compile(r'^(?P\w+)://(?P.*)$') +user_re = r'[^/?#@]+@' # r'(?P[^/?#@]+)@' # discarded for now +query_re = r'\??[^#]*' # r'\??(?P[^#]*)' # discarded for now +domain_re = r'(?P[^/?#]+)' +path_re = r'(?P[^?#]*)' +http_regex = re.compile(f'{domain_re}{path_re}{query_re}.*') +ftp_regex = re.compile(f'(?:{user_re})?{domain_re}{path_re}.*') + +class UrlError(ValueError): + pass + +class DeconstructedUrl: + '''Represents a deconstructed URL or URL pattern''' + def __init__(self, url): + self.url = url + + match = proto_regex.match(url) + if not match: + raise UrlError(_('invalid_URL_{}').format(url)) + + self.proto = match.group('proto') + if self.proto not in ('http', 'https', 'ftp'): + raise UrlError(_('disallowed_protocol_{}').format(proto)) + + if self.proto == 'ftp': + match = ftp_regex.match(match.group('rest')) + elif self.proto in ('http', 'https'): + match = http_regex.match(match.group('rest')) + + if not match: + raise UrlError(_('invalid_URL_{}').format(url)) + + self.domain = match.group('domain').split('.') + self.domain.reverse() + self.path = [*filter(None, match.group('path').split('/'))] + +class MappingItem: + ''' + A mapping, together with one of its patterns, as stored in Pattern Tree. + ''' + def __init__(self, pattern, mapping): + self.pattern = pattern + self.mapping = mapping + + def register(self, patterns_by_proto): + ''' + Make self queryable through the Pattern Tree that starts with the + protocols dictionary passed in the argument. + ''' + deco = DeconstructedUrl(self.pattern) + + domain_tree = patterns_by_proto.get(deco.proto) or PatternTreeNode() + patterns_by_proto[deco.proto] = domain_tree + + for path_tree in domain_tree.add(deco.domain, PatternTreeNode): + for match_list in path_tree.add(deco.path, list): + match_list.append(self) + +class Content: + '''Stores serveable website content.''' + def __init__(self): + self.resources = {} + self.mappings = {} + self.licenses = {} + self.indexes = {} + self.definition_processors = { + 'resource': self.process_resource_or_mapping, + 'mapping': self.process_resource_or_mapping, + 'license': self.process_license + } + self.patterns_by_proto = {} + + @staticmethod + def register_item(dict, item): + ''' + Helper function used to add a versioned item definition to content + data structures. + ''' + identifier = item['identifier'] + versioned_item = dict.get(identifier) + if versioned_item is None: + versioned_item = VersionedContentItem() + dict[identifier] = versioned_item + + versioned_item.register_item(item) + + @staticmethod + def _process_copyright_and_license(definition): + '''Helper function used by other process_*() methods.''' + for field in ['copyright', 'licenses']: + if definition[field] == 'auto': + raise MyNotImplError(f'"{{field}}": "auto"', + definition['source_name']) + + def process_resource_or_mapping(self, definition, index): + ''' + Sanitizes, autocompletes and registers serveable mapping/resource + definition. + ''' + definition['version'] = normalize_version(definition['version']) + + if definition['type'] == 'resource': + self._process_copyright_and_license(definition) + definition['dependencies'] = definition.get('dependencies', []) + self.register_item(self.resources, definition) + else: + self.register_item(self.mappings, definition) + + def process_license(self, license, index): + '''Sanitizes and registers serveable license definition.''' + identifier = license['identifier'] + if identifier in self.licenses: + raise ValueError(_('license_clash_{}').format(identifier)) + + self.licenses[identifier] = license + + def process_index(self, index, source_name): + ''' + Sanitizes, autocompletes and registers data from a loaded index.json + file. + ''' + schema_ver = normalize_version(index['schema_version']) + index['schema_version'] = schema_ver + if schema_ver != SCHEMA_VERSION: + raise ValueError('index_json_schema_mismatch_{found}_{required}' + .format(found=version_string(schema_ver), + required=version_string(SCHEMA_VERSION))) + + if source_name in self.indexes: + raise ValueError(_('source_name_clash_{}').format(source_name)) + + index['source_name'] = source_name + + self._process_copyright_and_license(index) + + self.indexes[source_name] = index + + for definition in index['definitions']: + try: + definition['source_name'] = source_name + definition['source_copyright'] = index['copyright'] + definition['source_licenses'] = index['licenses'] + processor = self.definition_processors[definition['type']] + processor(definition, index) + except Exception as e: + if current_app._pydrilla_werror: + raise e from None + logging.error(_('couldnt_load_definition_from_%s'), subdir_path, + exc_info=True) + @staticmethod + def all_items(versioned_items_dict): + '''Iterator over all registered versions of all items.''' + for versioned_item in versioned_items_dict.values(): + for item in versioned_item.by_version.values(): + yield item + + def report_missing(self): + ''' + Use logger to print information about items that are referenced but + were not loaded. + ''' + def report_missing_license(object, object_type, lic): + if object_type == 'index': + logging.error(_('no_index_license_%(source)s_%(lic)s'), + source=object['source_name'], lic=lic) + return + + ver_str = version_string(object['version']) + kwargs = {object_type: object['identifier'], ver: ver_str, lic: lic} + if object_type == 'resource': + fmt = _('no_resource_license_%(resource)s_%(ver)s_%(lic)s') + else: + fmt = _('no_mapping_license_%(mapping)s_%(ver)s_%(lic)s') + + logging.error(fmt, **kwargs) + + for object_type, iterable in [ + ('index', self.indexes.values()), + ('resource', self.all_items(self.resources)) + ]: + for object in iterable: + to_process = [object['licenses']] + licenses = [] + while to_process: + term = to_process.pop() + + if type(term) is str: + if term not in ['or', 'and'] and \ + term not in self.licenses: + report_missing_license(object, object_type, lic) + continue + + to_process.extend(term) + + def report_missing_dependency(resource, dep): + logging.error(_('no_dep_%(resource)s_%(ver)s_%(dep)s'), + dep=dep, resource=resource['identifier'], + ver=version_string(resource['version'])) + + for resource in self.all_items(self.resources): + for dep in resource['dependencies']: + if dep not in self.resources: + report_missing_dependency(resource, dep) + + def report_missing_payload(mapping, payload): + logging.error(_('no_payload_%(mapping)s_%(ver)s_%(payload)s'), + mapping=mapping['identifier'], payload=payload, + ver=version_string(mapping['version'])) + + for mapping in self.all_items(self.mappings): + for payload in mapping['payloads']: + payload = payload['payload'] + if payload not in self.resources: + report_missing_payload(mapping, payload) + + def finalize(self): + ''' + Initialize structures needed to serve queries. Called once after all + data gets loaded. + ''' + for dict in [self.resources, self.mappings]: + for versioned_item in dict.values(): + versioned_item.known_versions.sort() + + for mapping in self.all_items(self.mappings): + for payload in mapping['payloads']: + try: + MappingItem(pattern, mapping)\ + .register(self.patterns_by_proto) + except Exception as e: + if current_app._pydrilla_werror: + raise e from None + logging.error( + _('couldnt_register_%(mapping)s_%(ver)s_%(pattern)s'), + mapping=mapping['identifier'], pattern=pattern, + ver=version_string(mapping['version']) + ) + + def find_item(self, type, identifier, ver=None): + ''' + Find and return definition of the newest version of resource/mapping + named by identifier. If no such resource/mapping exists, return None. + + If ver is specified, instead find and return definition of that version + of the item (or None is absent). + ''' + dict = self.resources if type == 'resource' else self.mappings + versioned_item = dict.get(identifier) + if not versioned_item: + return None + + ver = version_string(ver or versioned_item.known_versions[-1]) + + return versioned_item.by_version.get(ver) + + def query(self, url, max=0): + ''' + Return return registered patterns and mappings (available as + MappingItems) that match url. The maximum number of items yielded may be + limited by using the optional max argument. Its default value, 0, causes + no limit to be imposed. + + If multiple versions of a mapping are applicable, only the most recent + is included in the result. + ''' + deco = DeconstructedUrl(url) + + domain_tree = self.patterns_by_proto.get(deco.proto) \ + or PatternTreeNode() + for path_tree in domain_tree.search(deco.domain): + for item in path_tree.search(deco.path): + if url[-1] == '/' or item.pattern[-1] != '/': + yield item + max -= 1 + if max == 0: + return + +def load_content_from_subdir(subdir_path, source_name, content): + index_path = subdir_path / 'index.json' + with open(index_path) as index_file: + index = json.loads(strip_json_comments(index_file.read())) + + content.process_index(index, source_name) + +def load_content(path): + path = pathlib.Path(path) + if not path.is_dir(): + raise ValueError(_('content_dir_path_not_dir')) + + content = Content() + + for subdir_path in path.iterdir(): + if not subdir_path.is_dir(): + continue + try: + load_content_from_subdir(subdir_path, subdir_path.name, content) + except Exception as e: + if current_app._pydrilla_werror: + raise e from None + logging.error(_('couldnt_load_content_from_%s'), subdir_path, + exc_info=True) + + content.report_missing() + content.finalize() + + return content + +def create_app(config_path=(here / 'config.json'), flask_config={}): + app = Flask(__package__) + app.config.update(flask_config) + + language = flask_config.get('lang', 'en') + translation = gettext.translation('pydrilla', localedir=(here / 'locales'), + languages=[language]) + + app._pydrilla_gettext = translation.gettext + + # https://stackoverflow.com/questions/9449101/how-to-stop-flask-from-initialising-twice-in-debug-mode + if app.debug and os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + return app + + config = load_config(config_path) + for key in ['static_resource_uri', 'content_dir']: + if key not in config: + raise ValueError(_('config_key_absent_{}').format(key)) + + app._pydrilla_static_resource_uri = config['static_resource_uri'] + app._pydrilla_werror = config.get('werror', False) + if 'hydrilla_parent' in config: + raise MyNotImplError('hydrilla_parent', config_path.name) + with app.app_context(): + app._pydrilla_content = load_content(config['content_dir']) + + app.register_blueprint(bp) + + return app + +def _(text_key): + return current_app._pydrilla_gettext(text_key) + +def escaping_gettext(text_key): + from markupsafe import escape + + return str(escape(_(text_key))) + +class MyEnvironment(Environment): + ''' + A wrapper class around jinja2.Environment that causes GNU gettext function + (as '_' and '__') and url_for function to be passed to every call of each + template's render() method. + ''' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_template(self, *args, **kwargs): + template = super().get_template(*args, **kwargs) + old_render = template.render + + def new_render(*args, **kwargs): + final_kwargs = { + '_': escaping_gettext, + '__': escaping_gettext, + 'url_for': url_for + } + final_kwargs.update(kwargs) + + return old_render(*args, **final_kwargs) + + template.render = new_render + + return template + +j2env = MyEnvironment(loader=PackageLoader(__package__), autoescape=False) + +indexpage = j2env.get_template('index.html') +@bp.route('/') +def index(): + return indexpage.render(content=current_app._pydrilla_resources_map) + +for item_type in ['resource', 'mapping']: + def item(identifier): + ver = request.args.get('ver') + if ver is not None: + try: + ver = normalize_version(parse_version(ver)) + except: + abort(400) + + item = current_app._pydrilla_content\ + .find_item(item_type, identifier, ver) + if item is None: + abort(404) + + return json.dumps(item) + + item.__name__ = item_type + 's' + bp.route(f'/{item_type}s/')(item) diff --git a/src/pydrilla/templates/base.html b/src/pydrilla/templates/base.html new file mode 100644 index 0000000..6e7887e --- /dev/null +++ b/src/pydrilla/templates/base.html @@ -0,0 +1,94 @@ +{# SPDX-License-Identifier: CC-BY-NC-SA-4.0 + +Base HTML page template. + +This file is part of Hydrilla + +Copyright (C) 2021 Wojtek Kosior + +This file is free cultural work: you can redistribute it with or +without modification under the terms of the Creative Commons +Attribution Share Alike 4.0 International as published by the +Creative Commons Corporation. + +This file 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 +Creative Commons Attribution Share Alike 4.0 International +License for more details. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's license. Although I request that you do not make use this code +in a proprietary program, I am not going to enforce this in court. +#} + +{% macro link_for(endpoint, text) -%} + + {{ text }} + +{%- endmacro %} + + + + + {% block head %} + + + {% block title %}{{ _('hydrilla') }}{% endblock %} + {% endblock %} + + + {% block body %} + + {% block content %} + {% endblock %} + {% endblock %} + + diff --git a/src/pydrilla/templates/index.html b/src/pydrilla/templates/index.html new file mode 100644 index 0000000..71de8ba --- /dev/null +++ b/src/pydrilla/templates/index.html @@ -0,0 +1,32 @@ +{# SPDX-License-Identifier: CC-BY-NC-SA-4.0 + +HTML index page template. + +This file is part of Hydrilla + +Copyright (C) 2021 Wojtek Kosior + +This file is free cultural work: you can redistribute it with or +without modification under the terms of the Creative Commons +Attribution Share Alike 4.0 International as published by the +Creative Commons Corporation. + +This file 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 +Creative Commons Attribution Share Alike 4.0 International +License for more details. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's license. Although I request that you do not make use this code +in a proprietary program, I am not going to enforce this in court. +#} + +{% extends 'base.html' %} +{% block body %} + {{ super() }} +

{{ _('hydrilla_welcome') }}

+

content

+ {{ content }} +{% endblock %} diff --git a/src/pydrilla_dev_helper.py b/src/pydrilla_dev_helper.py new file mode 100644 index 0000000..88dc63e --- /dev/null +++ b/src/pydrilla_dev_helper.py @@ -0,0 +1,293 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Definitions of helper commands to use with setuptools +# +# This file is part of Hydrilla +# +# Copyright (C) 2021 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this file's +# license. Although I request that you do not make use this code in a +# proprietary program, I am not going to enforce this in court. + +from setuptools import Command +from setuptools.command.build_py import build_py +import sys +from pathlib import Path +import subprocess +import re +import os +import json +import importlib + +def mypath(path_or_string): + return Path(path_or_string).resolve() + +debrel_regex = re.compile(r'^[^(]*\([^-]*-([^)]*)\)') + +def extract_debrel(debian_dir): + changelog_path = mypath(debian_dir) / 'changelog' + with open(changelog_path) as changelog_file: + try: + return debrel_regex.match(changelog_file.readline())[1] + except TypeError: + raise RuntimeException('Cannot extract debrel from %s.' % + changelog_path) + +class Helper: + def __init__(self, project_root, app_package_name, version, locales_dir, + locales=['en', 'pl'], default_locale='en', locale_domain=None, + packages_root=None, debian_dir=None, config_path=None): + self.project_root = mypath(project_root) + self.app_package_name = app_package_name + self.version = version + self.locales_dir = mypath(locales_dir) + self.locales = locales + self.default_locale = default_locale + self.locale_domain = locale_domain or app_package_name + self.packages_root = mypath(packages_root or project_root / 'src') + self.app_package_dir = self.packages_root / app_package_name + self.debian_dir = mypath(debian_dir or project_root / 'debian') + self.config_path = config_path and mypath(config_path) + self.locale_files_list = None + + def run_command(self, command, verbose, runner=subprocess.run, **kwargs): + cwd = kwargs.get('cwd') + if cwd: + cwd = mypath(cwd) + where = f'from {cwd} ' + else: + cwd = Path.cwd().resolve() + where = '' + + str_command = [str(command[0])] + + for arg in command[1:]: + if isinstance(arg, Path): + try: + arg = str(arg.relative_to(cwd)) + except ValueError: + arg = str(arg) + + str_command.append(arg) + + if verbose: + print(f'{where}executing {" ".join(str_command)}') + runner(str_command, **kwargs) + + def create_mo_files(self, dry_run=False, verbose=False): + self.locale_files_list = [] + + for locale in self.locales: + messages_dir = self.locales_dir / locale / 'LC_MESSAGES' + + for po_path in messages_dir.glob('*.po'): + mo_path = po_path.with_suffix('.mo') + + if not dry_run: + command = ['msgfmt', po_path, '-o', mo_path] + self.run_command(command, verbose=verbose, check=True) + + self.locale_files_list.extend([po_path, mo_path]) + + def locale_files(self): + if self.locale_files_list is None: + self.create_mo_files(dry_run=True) + + return self.locale_files_list + + def locale_files_relative(self, to=None): + if to is None: + to = self.app_package_dir + + return [file.relative_to(to) for file in self.locale_files()] + + def flask_run(self, locale=None): + for var, val in (('ENV', 'development'), ('DEBUG', 'True')): + os.environ[f'FLASK_{var}'] = os.environ.get(f'FLASK_{var}', val) + + config = {'lang': locale or self.default_locale} + + sys.path.insert(0, str(self.packages_root)) + package = importlib.import_module(self.app_package_name) + + # make relative paths in json config resolve from project's directory + os.chdir(self.project_root) + + kwargs = {'config_path': self.config_path} if self.config_path else {} + package.create_app(flask_config=config, **kwargs).run() + + def update_po_files(self, verbose=False): + pot_path = self.locales_dir / f'{self.locale_domain}.pot' + rglob = self.app_package_dir.rglob + command = ['xgettext', '-d', self.locale_domain, '--language=Python', + '-o', pot_path, *rglob('*.py'), *rglob('*.html')] + + self.run_command(command, verbose=verbose, check=True, + cwd=self.app_package_dir) + + for locale in self.locales: + messages_dir = self.locales_dir / locale / 'LC_MESSAGES' + + for po_path in messages_dir.glob('*.po'): + if po_path.stem != self.app_package_name: + continue; + + if po_path.exists(): + command = ['msgmerge', '--update', po_path, pot_path] + else: + command = ['cp', po_path, pot_path] + + self.run_command(command, verbose=verbose, check=True) + + if (verbose): + print('removing generated .pot file') + pot_path.unlink() + + # we exclude these from the source archive we produce + bad_file_regex = re.compile(r'^\..*|build|debian|dist') + + def make_tarballs(self, verbose=False): + name=self.app_package_name + ver=self.version + debrel=extract_debrel(self.debian_dir) + + source_dirname = f'{name}-{ver}' + source_tarball_name = f'{name}_{ver}.orig.tar.gz' + debian_tarball_name = f'{name}_{ver}-{debrel}.debian.tar.gz' + + source_args = [f'--prefix={source_dirname}/', '-o', + self.project_root.parent / source_tarball_name, 'HEAD'] + + for filepath in self.project_root.iterdir(): + if not self.bad_file_regex.search(filepath.parts[-1]): + source_args.append(filepath) + + debian_args = ['-o', self.project_root.parent / debian_tarball_name, + 'HEAD', self.debian_dir] + + for args in [source_args, debian_args]: + command = ['git', 'archive', '--format=tar.gz', *args] + self.run_command(command, verbose=verbose, check=True) + + def commands(self): + helper = self + + class MsgfmtCommand(Command): + '''A custom command to run msgfmt on all .po files below '{}'.''' + + description = 'use msgfmt to generate .mo files from .po files' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + helper.create_mo_files(verbose=self.verbose) + + MsgfmtCommand.__doc__ = MsgfmtCommand.__doc__.format(helper.locales_dir) + + class RunCommand(Command): + ''' + A custom command to run the app using flask. + + This is similar in effect to: + PYTHONPATH='{packages_root}' FLASK_APP={app_package_name} \\ + FLASK_ENV=development flask run + ''' + + description = 'run the Flask app from source directory' + + user_options = [ + ('locale=', 'l', + "app locale (one of: %s; default: '%s')" % + (', '.join([f"'{l}'" for l in helper.locales]), + helper.default_locale)) + ] + + def initialize_options(self): + self.locale = helper.default_locale + + def finalize_options(self): + if self.locale not in helper.locales: + raise ValueError("Locale '%s' not supported" % self.lang) + + def run(self): + helper.flask_run(locale=self.locale) + + RunCommand.__doc__ = RunCommand.__doc__.format( + packages_root=self.packages_root, + app_package_name=self.app_package_name + ) + + class MsgmergeCommand(Command): + ''' + A custom command to run xgettext and msgmerge to update project's + .po files below '{}'. + ''' + + description = 'use xgettext and msgmerge to update (or generate) .po files for this project' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + helper.update_po_files(verbose=self.verbose) + + MsgmergeCommand.__doc__ = \ + MsgmergeCommand.__doc__.format(helper.locales_dir) + + class TarballsCommand(Command): + ''' + A custom command to run git archive to create debian tarballs of + this project. + ''' + + description = 'use git archive to create .orig.tar.gz and .debian.tar.gz files for this project' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + helper.make_tarballs(verbose=self.verbose) + + class BuildCommand(build_py): + ''' + The build command but runs the custom msgfmt command before build. + ''' + def run(self, *args, **kwargs): + self.run_command('msgfmt') + super().run(*args, **kwargs) + + return { + 'msgfmt': MsgfmtCommand, + 'run': RunCommand, + 'msgmerge': MsgmergeCommand, + 'tarballs': TarballsCommand, + 'build_py': BuildCommand + } diff --git a/src/test/__init__.py b/src/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/test/test_pydrilla.py b/src/test/test_pydrilla.py new file mode 100644 index 0000000..0ed5fa9 --- /dev/null +++ b/src/test/test_pydrilla.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Repository tests +# +# This file is part of Hydrilla +# +# Copyright (C) 2021 Wojtek Kosior +# +# 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 . +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +import pytest +import sys +import shutil +from pathlib import Path +from os import mkdir, unlink, environ +import json + +import pydrilla + +test_dir = Path(__file__).resolve().parent +pydrilla_dir = Path(hydrilla.__file__).resolve().parent +test_config_path = test_dir / 'config.json' + +@pytest.fixture +def client(): + app = pydrilla.create_app(test_config_path, flask_config={'TESTING': True}) + + with app.test_client() as client: + yield client + +def test_basic(client): + response = client.get('/') + assert b'html' in response.data + +def test_normalize_version(): + assert pydrilla.normalize_version([4, 5, 3, 0, 0]) == [4, 5, 3] + assert pydrilla.normalize_version([1, 0, 5, 0]) == [1, 0, 5] + assert pydrilla.normalize_version([3, 3]) == [3, 3] + +### pad_versions() and compare_versions() likely won't be needed + +# def test_compare_versions(): +# compare_versions = pydrilla.compare_versions +# # without revision +# assert compare_versions([43], [43]) == 0 +# assert compare_versions([54], [34]) == 1 +# assert compare_versions([1], [3]) == -1 +# assert compare_versions([10, 2], [10, 2]) == 0 +# assert compare_versions([11, 6], [11, 2]) == 1 +# assert compare_versions([3, 0], [3, 8]) == -1 +# assert compare_versions([1, 2, 3], [1, 2]) == 1 +# assert compare_versions([1, 2], [1, 2, 3]) == -1 +# assert compare_versions([1], [1, 0, 0]) == 0 + +# # with revision +# assert compare_versions([43], [43], rev2=3) == -1 +# assert compare_versions([54], [34]), rev2=41) == 1 +# assert compare_versions([1], [3]), rev1=6) == -1 +# assert compare_versions([10, 2], [10, 2]), rev1=8, rev2=5) == 1 +# assert compare_versions([11, 6], [11, 2]), rev2=19) == 1 +# assert compare_versions([3, 0], [3, 8]), rev2=5) == -1 +# assert compare_versions([1, 2, 3], [1, 2]), rev1=4) == 1 +# assert compare_versions([1, 2], [1, 2, 3]), rev2=7) == -1 +# assert compare_versions([1], [1, 0, 0]), rev2=9, rev1=9) == 0 + +# from functools import cmp_to_key + +# versions = [[43], [54], [3, 0], [34], [3], [1], [4, 5, 3], [1, 0, 5], +# [3, 3], [10, 2], [11, 2], [11, 6], [3, 8], [1, 2], [1, 2, 3], +# [1, 0, 0]] +# versions.sort(cmp_to_key(compare_versions)) +# assert versions == [[1], [1, 0, 0], [1, 0, 5], [1, 2], [1, 2, 3], [3, 0], +# [3], [3, 3], [3, 8], [4, 5, 3], [10, 2], [11, 2], +# [11, 6], [34], [43], [54]] -- cgit v1.2.3