From 17c50e321ca3086f760acbc251ab11d69712a957 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 14 Dec 2023 01:18:57 +0100 Subject: [PATCH] Initial commit --- .gitignore | 157 ++++++++ .renovaterc | 20 ++ LICENSE | 674 +++++++++++++++++++++++++++++++++++ classes/exceptions.py | 96 +++++ classes/models.py | 25 ++ config_example.json | 9 + extensions/exceptions.py | 73 ++++ extensions/locations.py | 169 +++++++++ favicon.ico | Bin 0 -> 270398 bytes main.py | 25 ++ migrations/.gitkeep | 0 modules/app.py | 22 ++ modules/database.py | 26 ++ modules/extensions_loader.py | 53 +++ modules/scheduler.py | 3 + modules/utils.py | 71 ++++ requirements.txt | 10 + 17 files changed, 1433 insertions(+) create mode 100644 .gitignore create mode 100644 .renovaterc create mode 100644 LICENSE create mode 100644 classes/exceptions.py create mode 100644 classes/models.py create mode 100644 config_example.json create mode 100644 extensions/exceptions.py create mode 100644 extensions/locations.py create mode 100644 favicon.ico create mode 100644 main.py create mode 100644 migrations/.gitkeep create mode 100644 modules/app.py create mode 100644 modules/database.py create mode 100644 modules/extensions_loader.py create mode 100644 modules/scheduler.py create mode 100644 modules/utils.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e4bb6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Custom +.vscode +config.json \ No newline at end of file diff --git a/.renovaterc b/.renovaterc new file mode 100644 index 0000000..c416352 --- /dev/null +++ b/.renovaterc @@ -0,0 +1,20 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "baseBranches": [ + "dev" + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "automerge": true + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/classes/exceptions.py b/classes/exceptions.py new file mode 100644 index 0000000..357a775 --- /dev/null +++ b/classes/exceptions.py @@ -0,0 +1,96 @@ +from fastapi import HTTPException + + +class LocationNotFoundError(HTTPException): + """Raises HTTP 404 if no location with this ID found.""" + + def __init__(self, id: int): + self.id = id + self.openapi = { + "description": "Location Does Not Exist", + "content": { + "application/json": { + "example": {"detail": "Could not find location with id '{id}'."} + } + }, + } + + +class EntrySearchQueryEmptyError(HTTPException): + """Raises HTTP 422 if no entry search query provided.""" + + def __init__(self): + self.openapi = { + "description": "Invalid Query", + "content": { + "application/json": { + "example": { + "detail": "You must provide location and date(s) to look for entries." + } + } + }, + } + super().__init__( + status_code=422, + detail=self.openapi["content"]["application/json"]["example"]["detail"], + ) + + +class LocationSearchQueryEmptyError(HTTPException): + """Raises HTTP 422 if no location search query provided.""" + + def __init__(self): + self.openapi = { + "description": "Invalid Query", + "content": { + "application/json": { + "example": { + "detail": "You must provide name or coordinates to look for location." + } + } + }, + } + super().__init__( + status_code=422, + detail=self.openapi["content"]["application/json"]["example"]["detail"], + ) + + +class SearchLimitInvalidError(HTTPException): + """Raises HTTP 400 if search results limit not in valid range.""" + + def __init__(self): + self.openapi = { + "description": "Invalid Limit", + "content": { + "application/json": { + "example": { + "detail": "Parameter 'limit' must be greater or equal to 1." + } + } + }, + } + super().__init__( + status_code=400, + detail=self.openapi["content"]["application/json"]["example"]["detail"], + ) + + +class SearchPageInvalidError(HTTPException): + """Raises HTTP 400 if page or page size are not in valid range.""" + + def __init__(self): + self.openapi = { + "description": "Invalid Page", + "content": { + "application/json": { + "example": { + "detail": "Parameters 'page' and 'page_size' must be greater or equal to 1." + } + } + }, + } + super().__init__( + status_code=400, + detail=self.openapi["content"]["application/json"]["example"]["detail"], + ) diff --git a/classes/models.py b/classes/models.py new file mode 100644 index 0000000..18cba57 --- /dev/null +++ b/classes/models.py @@ -0,0 +1,25 @@ +from typing import List, Literal + +from pydantic import BaseModel + + +class CollectionEntry(BaseModel): + locations: List[int] + garbage_type: Literal[0, 1, 2, 3, 4, 5] + date: str + + +class Location(BaseModel): + id: int + name: str + location: List[int] + country: int + timezone: str + + +class SearchResultsCollectionEntry(BaseModel): + results: List[CollectionEntry] + + +class SearchResultsLocation(BaseModel): + results: List[Location] diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..c7472db --- /dev/null +++ b/config_example.json @@ -0,0 +1,9 @@ +{ + "database": { + "name": "photos", + "host": "127.0.0.1", + "port": 27017, + "user": null, + "password": null + } +} \ No newline at end of file diff --git a/extensions/exceptions.py b/extensions/exceptions.py new file mode 100644 index 0000000..31c5c23 --- /dev/null +++ b/extensions/exceptions.py @@ -0,0 +1,73 @@ +from fastapi import Request +from fastapi.responses import UJSONResponse +from starlette.status import ( + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, + HTTP_404_NOT_FOUND, + HTTP_422_UNPROCESSABLE_ENTITY, +) + +from classes.exceptions import ( + EntrySearchQueryEmptyError, + LocationNotFoundError, + LocationSearchQueryEmptyError, + SearchLimitInvalidError, + SearchPageInvalidError, +) +from modules.app import app + + +@app.exception_handler(LocationNotFoundError) +async def location_not_found_exception_handler( + request: Request, exc: LocationNotFoundError +): + return UJSONResponse( + status_code=HTTP_404_NOT_FOUND, + content={"detail": f"Could not find location with id '{exc.id}'."}, + ) + + +@app.exception_handler(EntrySearchQueryEmptyError) +async def entry_search_query_empty_exception_handler( + request: Request, exc: EntrySearchQueryEmptyError +): + return UJSONResponse( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "detail": "You must provide location and date(s) to look for entries." + }, + ) + + +@app.exception_handler(LocationSearchQueryEmptyError) +async def location_search_query_empty_exception_handler( + request: Request, exc: LocationSearchQueryEmptyError +): + return UJSONResponse( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "detail": "You must provide name or coordinates to look for location." + }, + ) + + +@app.exception_handler(SearchLimitInvalidError) +async def search_limit_invalid_exception_handler( + request: Request, exc: SearchLimitInvalidError +): + return UJSONResponse( + status_code=HTTP_401_UNAUTHORIZED, + content={"detail": "Parameter 'limit' must be greater or equal to 1."}, + ) + + +@app.exception_handler(SearchPageInvalidError) +async def search_page_invalid_exception_handler( + request: Request, exc: SearchPageInvalidError +): + return UJSONResponse( + status_code=HTTP_400_BAD_REQUEST, + content={ + "detail": "Parameters 'page' and 'page_size' must be greater or equal to 1." + }, + ) diff --git a/extensions/locations.py b/extensions/locations.py new file mode 100644 index 0000000..15b152f --- /dev/null +++ b/extensions/locations.py @@ -0,0 +1,169 @@ +import re +from datetime import datetime, timedelta +from typing import Literal, Union + +from fastapi.responses import UJSONResponse +from pymongo import ASCENDING + +from classes.exceptions import ( + EntrySearchQueryEmptyError, + LocationNotFoundError, + LocationSearchQueryEmptyError, + SearchPageInvalidError, +) +from classes.models import Location, SearchResultsCollectionEntry, SearchResultsLocation +from modules.app import app +from modules.database import col_entries, col_locations + +location_get_responses = { + 404: LocationNotFoundError(0).openapi, +} + + +@app.get( + "/locations/{id}", + description="Get a location by id", + response_model=Location, + response_class=UJSONResponse, + responses=location_get_responses, # type: ignore +) +async def location_get( + id: int, +): + location = await col_locations.find_one({"id": id}) + + if location is None: + raise LocationNotFoundError(id) + + del location["_id"] # type: ignore + + return UJSONResponse(location) + + +location_find_responses = { + 400: SearchPageInvalidError().openapi, + 422: LocationSearchQueryEmptyError().openapi, +} + + +@app.get( + "/locations", + description="Find a location by name or coordinates", + response_class=UJSONResponse, + response_model=SearchResultsLocation, + responses=location_find_responses, # type: ignore +) +async def location_find( + q: Union[str, None] = None, + page: int = 1, + page_size: int = 100, + lat: Union[float, None] = None, + lng: Union[float, None] = None, + radius: Union[int, None] = None, +): + if page <= 0 or page_size <= 0: + raise SearchPageInvalidError() + + output = {"results": []} + skip = (page - 1) * page_size + + radius = 5000 if radius is None else radius + + if (lat is not None) and (lng is not None): + db_query = { + "location": { + "$nearSphere": { + "$geometry": {"type": "Point", "coordinates": [lng, lat]}, + "$maxDistance": radius, + } + }, + } + elif q is not None: + db_query = {"name": re.compile(q)} + else: + raise LocationSearchQueryEmptyError() + + locations = [ + location + async for location in (col_locations.find(db_query, limit=page_size, skip=skip)) + ] + + for location in locations: + output["results"].append( + { + "id": location["id"], + "name": location["name"], + "location": location["location"], + "country": location["country"], + "timezone": location["timezone"], + } + ) + + return UJSONResponse(output) + + +entry_find_responses = { + 400: SearchPageInvalidError().openapi, + 404: LocationNotFoundError(0).openapi, + 422: LocationSearchQueryEmptyError().openapi, +} + + +@app.get( + "/locations/{location}/entries", + description="Find entries by date(s) or type", + response_class=UJSONResponse, + response_model=SearchResultsCollectionEntry, + responses=entry_find_responses, # type: ignore +) +async def entry_find( + location: int, + garbage_type: Union[Literal[0, 1, 2, 3, 4, 5], None] = None, + date_start: str = datetime.now().isoformat(), + date_end: str = (datetime.now() + timedelta(days=30)).isoformat(), + page: int = 1, + page_size: int = 100, +): + if (await col_locations.find_one({"id": location})) is None: + raise LocationNotFoundError(location) + + if page <= 0 or page_size <= 0: + raise SearchPageInvalidError() + + output = {"results": []} + skip = (page - 1) * page_size + + date_start_dt = datetime.fromisoformat(date_start) + date_end_dt = datetime.fromisoformat(date_end) + + if garbage_type is None and date_start_dt is None and date_end_dt is None: + raise EntrySearchQueryEmptyError() + elif garbage_type is None: + db_query = { + "locations": location, + "date": {"$gte": date_start_dt, "$lte": date_end_dt}, + } + else: + db_query = { + "locations": location, + "garbage_type": garbage_type, + "date": {"$gte": date_start_dt, "$lte": date_end_dt}, + } + + entries = [ + entry + async for entry in col_entries.find(db_query, limit=page_size, skip=skip).sort( + key="date", direction=ASCENDING + ) + ] + + for entry in entries: + output["results"].append( + { + "locations": entry["locations"], + "garbage_type": entry["garbage_type"], + "date": entry["date"].isoformat(), + } + ) + + return UJSONResponse(output) diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..81304db624591769bed5d207e832feab54013a6a GIT binary patch literal 270398 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 Z2ksrXci`TEdk5|vxOd>*fnR Any: + """Load json file + + ### Args: + * filepath (`Union[str, Path]`): Path to input file + + ### Returns: + * `Any`: Some json deserializable + """ + with open(filepath, "r", encoding="utf8") as file: + try: + output = loads(file.read()) + except JSONDecodeError: + logger.error( + "Could not load json file %s: file seems to be incorrect!\n%s", + filepath, + format_exc(), + ) + raise + except FileNotFoundError: + logger.error( + "Could not load json file %s: file does not seem to exist!\n%s", + filepath, + format_exc(), + ) + raise + file.close() + return output + + +def json_write(contents: Union[list, dict], filepath: Union[str, Path]) -> None: + """Save contents into json file + + ### Args: + * contents (`Union[list, dict]`): Some json serializable + * filepath (`Union[str, Path]`): Path to output file + """ + try: + with open(filepath, "w", encoding="utf8") as file: + file.write(dumps(contents, ensure_ascii=False, indent=4)) + file.close() + except Exception as exp: + logger.error("Could not save json file %s: %s\n%s", filepath, exp, format_exc()) + return + + +def config_get(key: str, *args: str) -> Any: + """Get value of the config key + + ### Args: + * key (`str`): The last key of the keys path. + * *args (`str`): Path to key like: dict[args][key]. + + ### Returns: + * `Any`: Value of provided key + """ + this_dict = json_load(Path("config.json")) + this_key = this_dict + for dict_key in args: + this_key = this_key[dict_key] + return this_key[key] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6bcebf3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +aiofiles==23.2.1 +apscheduler~=3.10.4 +fastapi[all]~=0.105.0 +passlib~=1.7.4 +pymongo~=4.6.1 +python-jose[cryptography]~=3.3.0 +slowapi==0.1.8 +ujson~=5.9.0 +--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple +async_pymongo==0.1.4 \ No newline at end of file