aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE661
-rw-r--r--README.md62
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control21
-rw-r--r--debian/copyright6
-rwxr-xr-xdebian/rules16
-rw-r--r--debian/source/format1
-rw-r--r--example_content/hello/bye.js7
-rw-r--r--example_content/hello/cc0.txt121
-rw-r--r--example_content/hello/hello.js7
-rw-r--r--example_content/hello/index.json299
-rw-r--r--example_content/hello/message.js8
-rw-r--r--pytest.ini12
-rwxr-xr-xsetup.py76
-rw-r--r--src/conftest.py0
-rw-r--r--src/pydrilla/__init__.py1
-rw-r--r--src/pydrilla/config.json13
-rw-r--r--src/pydrilla/development_config.json24
-rw-r--r--src/pydrilla/locales/en/LC_MESSAGES/pydrilla.po127
-rw-r--r--src/pydrilla/pydrilla.py700
-rw-r--r--src/pydrilla/templates/base.html94
-rw-r--r--src/pydrilla/templates/index.html32
-rw-r--r--src/pydrilla_dev_helper.py293
-rw-r--r--src/test/__init__.py0
-rw-r--r--src/test/test_pydrilla.py90
26 files changed, 2677 insertions, 0 deletions
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. <https://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <https://www.gnu.org/licenses/>.
+
+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
+<https://www.gnu.org/licenses/>.
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 <koszko@koszko.org> 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 <koszko@koszko.org>
+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
+ // <https://www.gnu.org/licenses/>.
+ }
+ ]
+}
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
--- /dev/null
+++ b/src/conftest.py
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 <koszko@koszko.org>\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 <https://www.gnu.org/licenses/>.
+#
+#
+# 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<proto>\w+)://(?P<rest>.*)$')
+user_re = r'[^/?#@]+@' # r'(?P<user>[^/?#@]+)@' # discarded for now
+query_re = r'\??[^#]*' # r'\??(?P<query>[^#]*)' # discarded for now
+domain_re = r'(?P<domain>[^/?#]+)'
+path_re = r'(?P<path>[^?#]*)'
+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/<string:identifier>')(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) -%}
+ <a href="{{ url_for(endpoint, **kwargs) }}"
+ {{ caller() if caller is defined }}>
+ {{ text }}
+ </a>
+{%- endmacro %}
+
+<!DOCTYPE html>
+<html>
+ <head>
+ {% block head %}
+ <meta http-equiv="Content-Security-Policy" content="script-src 'none';">
+ <style>
+ {% block styles %}
+ html, body, div, h1, h2, h3, h4, h5, h6 {
+ margin: 0;
+ padding: 0;
+ }
+
+ * {
+ color: #444;
+ }
+
+ aside {
+ display: inline-block;
+ border-left: 0.2em solid #e44;
+ background-color: #edc;
+ padding: 0.2em;
+ }
+
+ .nav {
+ background-color: #ddd;
+ }
+
+ .nav>*:hover {
+ background-color: #999;
+ }
+
+ .nav>* {
+ display: inline-block;
+ padding: 1em;
+ }
+
+ .nav a {
+ text-decoration: none;
+ }
+
+ .home_link {
+ font-weight: bold;
+ font-size: 1.5em;
+ padding: 0.5em;
+ }
+ {% endblock %}
+ </style>
+ <title>{% block title %}{{ _('hydrilla') }}{% endblock %}</title>
+ {% endblock %}
+ </head>
+ <body>
+ {% block body %}
+ <div class="nav">
+ {% call link_for('bp.index', _('hydrilla')) %}
+ class="home_link"
+ {% endcall %}
+ </div>
+ {% block content %}
+ {% endblock %}
+ {% endblock %}
+ </body>
+</html>
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() }}
+ <h2>{{ _('hydrilla_welcome') }}</h2>
+ <h4>content</h4>
+ {{ 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 <https://www.gnu.org/licenses/>.
+#
+#
+# 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
--- /dev/null
+++ b/src/test/__init__.py
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 <https://www.gnu.org/licenses/>.
+#
+#
+# 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]]