commit 27fc1f5e1612d90a9e181eb98eb77481483133b7 Author: Mike Phares Date: Fri Jun 24 12:14:10 2022 -0700 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b75623 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb9ac05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Docker image for barcode-server + +FROM python:3.9 + +WORKDIR /app + +COPY . . + +RUN apt-get update \ + && apt-get -y install sudo +RUN pip install --upgrade pip;\ + pip install pipenv;\ + pipenv install --system --deploy;\ + pip install . + +ENV PUID=1000 PGID=1000 + +ENTRYPOINT [ "docker/entrypoint.sh", "barcode-server" ] +CMD [ "run" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1a3195a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include Pipfile.lock \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1e8a77d --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +PROJECT=barcode_server + +current-version: + set -ex + @echo "Current version is `cat ${PROJECT}/__init__.py | grep '__version__' | cut -d ' ' -f3 | sed s/\\\"//g`" + +build: + git stash + python setup.py sdist + - git stash pop + +test: + pipenv run pytest + +git-release: + set -ex + git add ${PROJECT}/__init__.py + git commit -m "Bumped version to `cat ${PROJECT}/__init__.py | grep '__version__' | cut -d ' ' -f3 | sed s/\\\"//g`" + git tag v`cat ${PROJECT}/__init__.py | grep '__version__' | cut -d ' ' -f3 | sed s/\"//g` + git push + git push --tags + +_release-patch: + @echo "__version__ = \"`cat ${PROJECT}/__init__.py | awk -F '("|")' '{ print($$2)}' | awk -F. '{$$NF = $$NF + 1;} 1' | sed 's/ /./g'`\"" > ${PROJECT}/__init__.py +release-patch: test _release-patch git-release current-version + +_release-minor: + @echo "__version__ = \"`cat ${PROJECT}/__init__.py | awk -F '("|")' '{ print($$2)}' | awk -F. '{$$(NF-1) = $$(NF-1) + 1;} 1' | sed 's/ /./g' | awk -F. '{$$(NF) = 0;} 1' | sed 's/ /./g' `\"" > ${PROJECT}/__init__.py +release-minor: test _release-minor git-release current-version + +_release-major: + @echo "__version__ = \"`cat ${PROJECT}/__init__.py | awk -F '("|")' '{ print($$2)}' | awk -F. '{$$(NF-2) = $$(NF-2) + 1;} 1' | sed 's/ /./g' | awk -F. '{$$(NF-1) = 0;} 1' | sed 's/ /./g' | awk -F. '{$$(NF) = 0;} 1' | sed 's/ /./g' `\"" > ${PROJECT}/__init__.py +release-major: test _release-major git-release current-version + +release: release-patch diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1032348 --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +container-app-conf = ">=5.0.0" +evdev = "*" +orjson = ">=3,<4" +aiohttp = "*" +asyncio-mqtt = "*" +prometheus-client = "*" +prometheus_async = "*" +click = "*" + +[dev-packages] +pytest = "*" +pytest-aiohttp = "*" +pytest-mock = "*" + +[requires] +python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..91e4e50 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,1004 @@ +{ + "_meta": { + "hash": { + "sha256": "276f2324151eac14f94c496e63c24d7da78b615150e77adb8339d5b223f1cc42" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + ], + "index": "pypi", + "version": "==3.8.1" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" + }, + "async-timeout": { + "hashes": [ + "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", + "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.2" + }, + "asyncio-mqtt": { + "hashes": [ + "sha256:6dbf85a45f94d26e7465411680ada3947b50546e98208d5d52e7a0a7ed7a7c38", + "sha256:cfa32fbd3a2d727ac74fa17ccafd30830a2a66f9e5e6ac7eee5458a9c7a6a832" + ], + "index": "pypi", + "version": "==0.12.1" + }, + "attrs": { + "hashes": [ + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" + ], + "markers": "python_version >= '3.6'", + "version": "==2.1.0" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "index": "pypi", + "version": "==8.1.3" + }, + "container-app-conf": { + "hashes": [ + "sha256:50e20bd0f8f124391769831272748a00f135dbb68592813fbb0c11ba6c437dd6", + "sha256:7b31a0f0501bf489dcab9a08df119ac63f387c49be65ed5a0914b6492d6f5bfd" + ], + "index": "pypi", + "version": "==5.2.2" + }, + "evdev": { + "hashes": [ + "sha256:5b33b174f7c84576e7dd6071e438bf5ad227da95efd4356a39fe4c8355412fe6" + ], + "index": "pypi", + "version": "==1.5.0" + }, + "frozenlist": { + "hashes": [ + "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e", + "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08", + "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b", + "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486", + "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78", + "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468", + "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1", + "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953", + "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3", + "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d", + "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a", + "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141", + "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08", + "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07", + "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa", + "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa", + "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868", + "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f", + "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b", + "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b", + "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1", + "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f", + "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478", + "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58", + "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01", + "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8", + "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d", + "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676", + "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274", + "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab", + "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8", + "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24", + "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a", + "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2", + "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f", + "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f", + "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93", + "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1", + "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51", + "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846", + "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5", + "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d", + "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c", + "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e", + "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae", + "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02", + "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0", + "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b", + "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3", + "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b", + "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa", + "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a", + "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d", + "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed", + "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148", + "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9", + "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c", + "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", + "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3.5'", + "version": "==3.3" + }, + "multidict": { + "hashes": [ + "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", + "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", + "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", + "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", + "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", + "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", + "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", + "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", + "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", + "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", + "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", + "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", + "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", + "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", + "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", + "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", + "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", + "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", + "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", + "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", + "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", + "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", + "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", + "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", + "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", + "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", + "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", + "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", + "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", + "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", + "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", + "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", + "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", + "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", + "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", + "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", + "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", + "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", + "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", + "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", + "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", + "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", + "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", + "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", + "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", + "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", + "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", + "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", + "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", + "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", + "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", + "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", + "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", + "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", + "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", + "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", + "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", + "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", + "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.2" + }, + "orjson": { + "hashes": [ + "sha256:0a4d747a43c5c9240968f74737da9f9978840f9b5c86bb578919c4e1adb05142", + "sha256:1594555a73670837a94a42ec21470d30a5cbe430bf1683f491b1f87270aaa305", + "sha256:1b8391a4fb5783fc8cd96f9594d64524204569dec612689e64cabfb6075bffe1", + "sha256:1cd9cd9bb9cee39aca4f7e1079cc3e180ffad0b13342ac3fabf5ff3caf7285bb", + "sha256:2cc17eb506613dda54061d956c416720dcd660ef6291ff94a88dcbb857933b2d", + "sha256:2efd7561e3a25bf381ee6410c4ca26f3425948074d394fae708bc1b7d2c09100", + "sha256:320c0ffe508a12e83631e8172c29f42af6ba52473f1c6afec0bc99831d3e7cd9", + "sha256:38cb1191aae1f62665750f2457d04a8d7b1d289745b1ff3617f665f24cbab3e7", + "sha256:4205c32a003c732b4fa9c040cd5a0660d87d38a7a97eb8db559d0b22f66bade6", + "sha256:4cb6ea393398e5665fd84f4bff11fff79fe42c40cc4b38afd8241b15aa72e2c3", + "sha256:4d10459578c139f428adedff41a78afde7034e277979cbb67b2d0584ddaff320", + "sha256:5738ab3283e75953b2aba549ebe7061cb883b2fd2d281097009242b3936c0114", + "sha256:6998ff27b9b9aaa4906f6ad40c6c973339c772143f56e8e920f4843fd6c04e75", + "sha256:6c59aec36cb77634dd218789bffa801f265a9522cf6311573ae4202a6888abca", + "sha256:6c7eca150d9d7dedf405d82c346105d6475e57789f3c3b4e0d12d59def94cb30", + "sha256:6ca694b989da69d31f129992bc00310b704adcbde8b3a24b571d883d2783a8e1", + "sha256:716c8f7eb39128067355bd1bc5e66156934d70295f5c62786a89af9c3f50c4d7", + "sha256:7406f8d163e7e7f358b34016e828f6b5ce15e6f7f0de3f82089a26ba1b41d75e", + "sha256:7592bc8c2009a7ee39ec1a022bb711f23bf74b9c49ffbe93479cdac16842b5ca", + "sha256:837bcaefb9ba61d40587d25052e3c74652e853108af6e2bc172c001b5f2ffd3e", + "sha256:863b7f13d1fd05b65ba4926f017b0d301d6b898db4ebcb19609af111f77a803c", + "sha256:8ef7696c3771dd11df5a45922794ad17b0421bfb5ba07832b960b653225ca739", + "sha256:940184f159f6be00e2f4320fdeebe6b842228db26db679c7cad5654818643938", + "sha256:95fd418e3e93305b728e056fa35096b70261b63ba6df404a8f6af5694b27de20", + "sha256:99b9c2ed45c06c77a990cd5d37bf43ac815fcb5cc36a2351629d270673ea4b94", + "sha256:9e68c01d576aaf3d83b3a18a3d2139d2cbfebc415264d7054d37d7fd8292bd08", + "sha256:9f2248d54957a63cf861285ea46f89ae79853e84ee26593560e8d53892a6b652", + "sha256:a4eb1ea1afe6494af75e61bf8bbca251e8e4883f1c235652e509e86e0eaf23b6", + "sha256:a9895be514b3aad76f81c739b7348c31f913971923ac71e0a35443abe6696a6d", + "sha256:a9d68a29111d9e2e9ac45eb339ae4083a754fc9deba3270ef573c87c64e75e3b", + "sha256:ade22b22f2d4baafcdcac63a66893250b51e7601e16f2c93bab29b30d233c5ac", + "sha256:ae229eef7a5a418011627b283d7fe0316d05fef65e88ae6623eecd5c6c701726", + "sha256:b2dadb4c0e2c4aa81f5bf20d58bd3b742e55c1142dd2bde5707749e77e3a9ad1", + "sha256:b500e4b1a6e8d93e226c55848419146365b57000f0afb1766475cb0a5b913aac", + "sha256:c06d6162694a83ba2f3a5fdceca9f8a7b30cae78e6e9651c8bdfb33cae3b95a3", + "sha256:c70e650324ce3efd65c9452c3e59c40f41e28a16a84ed4a56cb4ed84b4d788db", + "sha256:e17adc831c2a909a31b732e59973b6162d969f2e742a7c2428e62cc5fd65e00e" + ], + "index": "pypi", + "version": "==3.7.3" + }, + "paho-mqtt": { + "hashes": [ + "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f" + ], + "version": "==1.6.1" + }, + "prometheus-async": { + "hashes": [ + "sha256:5cbfa535561342b834c087c4f3f3be0a3cb8785a0b8748111c916f3d68bbc370", + "sha256:b0426370eb3b3bacd99afcf1fcc669c118cb67603cc951a6fe12434e9d4307f2" + ], + "index": "pypi", + "version": "==22.2.0" + }, + "prometheus-client": { + "hashes": [ + "sha256:522fded625282822a89e2773452f42df14b5a8e84a86433e3f8a189c1d54dc01", + "sha256:5459c427624961076277fdc6dc50540e2bacb98eebde99886e59ec55ed92093a" + ], + "index": "pypi", + "version": "==0.14.1" + }, + "py-range-parse": { + "hashes": [ + "sha256:15cf56ab4483814162f57f3b2bfd3ae68f6c4ea4cd7e710c881a5ce97f516c2c", + "sha256:a9d6b66f8e10dd26f5ff9726daef5b70bb32368a00a4563bd65d062c73d7c979" + ], + "version": "==1.0.5" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "pytimeparse": { + "hashes": [ + "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd", + "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a" + ], + "version": "==1.1.8" + }, + "ruamel.yaml": { + "hashes": [ + "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7", + "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af" + ], + "markers": "python_version >= '3'", + "version": "==0.17.21" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd", + "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee", + "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0", + "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7", + "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277", + "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104", + "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd", + "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0", + "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78", + "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de", + "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99", + "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527", + "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84", + "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7", + "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468", + "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b", + "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94", + "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233", + "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb", + "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5", + "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe", + "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751", + "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502", + "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed", + "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c" + ], + "markers": "python_version >= '3.5'", + "version": "==0.2.6" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "voluptuous": { + "hashes": [ + "sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6", + "sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723" + ], + "version": "==0.13.1" + }, + "wrapt": { + "hashes": [ + "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", + "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", + "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", + "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", + "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", + "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", + "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", + "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", + "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", + "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", + "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", + "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", + "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", + "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", + "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", + "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", + "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", + "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", + "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", + "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", + "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", + "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", + "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", + "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", + "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", + "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", + "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", + "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", + "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", + "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", + "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", + "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", + "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", + "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", + "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", + "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", + "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", + "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", + "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", + "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", + "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", + "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", + "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", + "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", + "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", + "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", + "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", + "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", + "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", + "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", + "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", + "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", + "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", + "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", + "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", + "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", + "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", + "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", + "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", + "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", + "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", + "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", + "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", + "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.14.1" + }, + "yarl": { + "hashes": [ + "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", + "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", + "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", + "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", + "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", + "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", + "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", + "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", + "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", + "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", + "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", + "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", + "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", + "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", + "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", + "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", + "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", + "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", + "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", + "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", + "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", + "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", + "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", + "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", + "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", + "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", + "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", + "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", + "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", + "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", + "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", + "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", + "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", + "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", + "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", + "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", + "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", + "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", + "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", + "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", + "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", + "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", + "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", + "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", + "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", + "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", + "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", + "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", + "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", + "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", + "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", + "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", + "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", + "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", + "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", + "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", + "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", + "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", + "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", + "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", + "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", + "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", + "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", + "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", + "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", + "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", + "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", + "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", + "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", + "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", + "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", + "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" + ], + "markers": "python_version >= '3.6'", + "version": "==1.7.2" + } + }, + "develop": { + "aiohttp": { + "hashes": [ + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + ], + "index": "pypi", + "version": "==3.8.1" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" + }, + "async-timeout": { + "hashes": [ + "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", + "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.2" + }, + "attrs": { + "hashes": [ + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" + ], + "markers": "python_version >= '3.6'", + "version": "==2.1.0" + }, + "frozenlist": { + "hashes": [ + "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e", + "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08", + "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b", + "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486", + "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78", + "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468", + "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1", + "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953", + "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3", + "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d", + "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a", + "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141", + "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08", + "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07", + "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa", + "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa", + "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868", + "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f", + "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b", + "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b", + "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1", + "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f", + "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478", + "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58", + "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01", + "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8", + "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d", + "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676", + "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274", + "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab", + "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8", + "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24", + "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a", + "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2", + "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f", + "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f", + "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93", + "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1", + "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51", + "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846", + "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5", + "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d", + "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c", + "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e", + "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae", + "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02", + "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0", + "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b", + "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3", + "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b", + "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa", + "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a", + "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d", + "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed", + "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148", + "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9", + "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c", + "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", + "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3.5'", + "version": "==3.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "multidict": { + "hashes": [ + "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", + "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", + "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", + "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", + "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", + "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", + "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", + "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", + "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", + "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", + "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", + "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", + "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", + "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", + "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", + "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", + "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", + "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", + "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", + "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", + "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", + "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", + "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", + "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", + "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", + "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", + "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", + "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", + "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", + "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", + "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", + "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", + "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", + "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", + "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", + "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", + "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", + "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", + "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", + "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", + "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", + "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", + "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", + "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", + "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", + "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", + "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", + "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", + "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", + "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", + "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", + "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", + "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", + "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", + "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", + "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", + "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", + "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", + "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.2" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, + "pytest": { + "hashes": [ + "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", + "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" + ], + "index": "pypi", + "version": "==7.1.2" + }, + "pytest-aiohttp": { + "hashes": [ + "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95", + "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4" + ], + "index": "pypi", + "version": "==1.0.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213", + "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91", + "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84" + ], + "markers": "python_version >= '3.7'", + "version": "==0.18.3" + }, + "pytest-mock": { + "hashes": [ + "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534", + "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231" + ], + "index": "pypi", + "version": "==3.7.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.1" + }, + "yarl": { + "hashes": [ + "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", + "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", + "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", + "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", + "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", + "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", + "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", + "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", + "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", + "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", + "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", + "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", + "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", + "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", + "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", + "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", + "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", + "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", + "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", + "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", + "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", + "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", + "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", + "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", + "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", + "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", + "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", + "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", + "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", + "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", + "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", + "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", + "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", + "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", + "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", + "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", + "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", + "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", + "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", + "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", + "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", + "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", + "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", + "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", + "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", + "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", + "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", + "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", + "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", + "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", + "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", + "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", + "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", + "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", + "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", + "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", + "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", + "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", + "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", + "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", + "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", + "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", + "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", + "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", + "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", + "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", + "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", + "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", + "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", + "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", + "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", + "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" + ], + "markers": "python_version >= '3.6'", + "version": "==1.7.2" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8dc611 --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# barcode-server [![Code Climate](https://codeclimate.com/github/markusressel/barcode-server.svg)](https://codeclimate.com/github/markusressel/barcode-server) + +A simple daemon to read barcodes from USB Barcode Scanners +and expose them to other service using HTTP calls, a websocket API or MQTT. + +[![asciicast](https://asciinema.org/a/366004.svg)](https://asciinema.org/a/366004) + +# Features + +* [x] Autodetect Barcode Scanner devices on the fly +* [x] Request Server information via [REST API](#rest-api) +* [x] Subscribe to barcode events using + * [x] [Websocket API](#websocket-api) +* [x] Push barcode events using + * [x] [HTTP requests](#http-request) + * [x] [MQTT messages](#mqtt-publish) +* [x] Get [statistics](#statistics) via Prometheus exporter + + +# How to use + +## Device Access Permissions + +Ensure the user running this application is in the correct group for accessing +input devices (usually `input`), like this: +``` +sudo usermod -a -G input myusername +``` + +## Configuration + +**barcode-server** uses [container-app-conf](https://github.com/markusressel/container-app-conf) +to provide configuration via a YAML or TOML file as well as ENV variables. Have a look at the +[documentation about it](https://github.com/markusressel/container-app-conf). + +The config file is searched for in the following locations (in this order): +* `./` +* `~/.config/` +* `~/` + +See [barcode_server.yaml](/barcode_server.yaml) for an example in this repo. + +## Native + +``` +# create venv +python -m venv ./venv +# enter venv +source ./venv/bin/activate +# install barcode-server +pip install barcode-server +# exit venv +deactivate + +# print config +./venv/bin/barcode-server config + +# launch application +./venv/bin/barcode-server run +``` + +## Docker + +When starting the docker container, make sure to pass through input devices: +``` +docker run -it \ + --name barcode \ + --device=/dev/input \ + -v "/home/markus/.config/barcode_server.yaml:/app/barcode_server.yaml" \ + -e PUID=0 \ + -e PGID=0 \ + markusressel/barcode-server +``` +**Note:** Although **barcode-server** will continuously try to detect new devices, +even when passing through `/dev/input` like shown above, new devices can not be detected +due to the way docker works. If you need to detect devices in real-time, you have to use +the native approach. + +You can specify the user id and group id using the `PUID` and `PGID` environment variables. + +# Webserver + +By default the webserver will listen to `127.0.0.1` on port `9654`. + +## Authorization + +When specified in the config, an API token is required to authorize clients, which must +be passed using a `X-Auth-Token` header when connecting. Since barcode-scanner doesn't rely on any +persistence, the token is specified in the configuration file and can not be changed on runtime. + +## Rest API + +**barcode-server** provides a simple REST API to get some basic information. +This API can **not** be used to retrieve barcode events. To do that you have to use one of +the approaches described below. + +| Endpoint | Description | +|------------|-------------------------------------------| +| `/devices` | A list of all currently detected devices. | + +## Websocket API + +In addition to the REST API **barcode-server** also exposes a websocket at `/`, which can be used +to get realtime barcode scan events. + +To connect to it, you have to provide + +* a `Client-ID` header with a UUID (v4) +* (optional) an empty `Drop-Event-Queue` header, to ignore events that happened between connections +* (optional) an `X-Auth-Token` header, to authorize the client + +Messages received on this websocket are JSON formatted strings with the following format: +```json +{ + "id": "33cb5677-3d0b-4faf-9dc4-d19a8ee7d8a1", + "serverId": "cash-register-1", + "date": "2020-08-03T10:00:00+00:00", + "device": { + "name": "BARCODE SCANNER BARCODE SCANNER", + "path": "/dev/input/event3", + "vendorId": "ffff", + "productId": "0035", + }, + "barcode": "4250168519463" +} +``` + +To test the connection you can use f.ex. `websocat`: + +``` +> websocat - autoreconnect:ws://127.0.0.1:9654 --text --header "Client-ID:dc1f14fc-a7a6-4102-af60-2b6e0dcf744c" --header "Drop-Event-Queue:" --header "X-Auth-Token:EmUSqjXGfnQwn5wn6CpzJRZgoazMTRbMNgH7CXwkQG7Ph7stex" +{"date":"2020-12-20T19:35:04.769739","device":{"name":"BARCODE SCANNER BARCODE SCANNER","path":"/dev/input/event3","vendorId":65535,"productId":53},"barcode":"D-t38409355843o52230Lm54784"} +{"date":"2020-12-20T19:35:06.237408","device":{"name":"BARCODE SCANNER BARCODE SCANNER","path":"/dev/input/event3","vendorId":65535,"productId":53},"barcode":"4250168519463"} +``` + +## HTTP Request + +When configured, you can let **barcode-scanner** issue a HTTP request (defaults to `POST`) when a +barcode is scanned, which provides the ability to push barcode events to a server that is unaware +of any client. The body of the request will contain the same JSON as in the websocket API example. + +To do this simply add the following section to your config: +```yaml +barcode_server: + [...] + http: + url: "https://my.domain.com/barcode" +``` + +Have a look at the [example config](barcode_server.yaml) for more options. + +## MQTT Publish + +When configured, you can let **barcode-scanner** publish barcode events to a MQTT broker. +The payload of the message will contain the same JSON as in the websocket API example. + +To do this simply add the following section to your config: +```yaml +barcode_server: + [...] + mqtt: + host: "my.mqtt.broker" +``` + +Have a look at the [example config](barcode_server.yaml) for more options. + +## Statistics + +**barcode-server** exposes a prometheus exporter (defaults to port `8000`) to give some statistical insight. +A brief overview of (most) available metrics: + +| Name | Type | Description | +|------|------|-------------| +| websocket_client_count | Gauge | Number of currently connected websocket clients | +| devices_count | Gauge | Number of currently detected devices | +| scan_count | Gauge | Number of times a scan has been detected | +| device_detection_processing_seconds | Summary | Time spent detecting devices | +| rest_endpoint_processing_seconds | Summary | Time spent in a rest command handler | +| notifier_processing_seconds | Summary | Time spent in a notifier | + +# FAQ + +## Can I lock the Barcode Scanner to this application? + +Yes. Most barcode readers normally work like a keyboard, resulting in their input being evaluated by +the system, which can clutter up your TTY or other open programs. +**barcode-server** will try to _grab_ input devices, making it the sole recipient of all +incoming input events from those devices, which should prevent the device from cluttering +your TTY. + +If, for some reason, this does not work for you, try this: + +Create a file `/etc/udev/rules.d/10-barcode.rules`: +``` +SUBSYSTEM=="input", ACTION=="add", ATTRS{idVendor}=="xxxx", ATTRS{idProduct}=="yyyy", RUN+="/bin/sh -c 'echo remove > /sys$env{DEVPATH}/uevent'" +SUBSYSTEM=="input", ACTION=="add", ATTRS{idVendor}=="xxxx", ATTRS{idProduct}=="yyyy", DEVPATH=="*:1.0/*", KERNEL=="event*", RUN+="/bin/sh -c 'ln -sf /dev/input/$kernel /dev/input/barcode_scanner'" +``` +Replace the `idVendor` and `idProduct` values with the values of your barcode reader (a 4 digit hex value with leading zeros). +You can find them in the log output of **barcode-reader** or using `lsusb` with the wireless receiver attached to your computer. + +Reload udev rules using: +``` +udevadm control --reload +``` +then remove and reinsert the wireless receiver. +You should now have a symlink in `/dev/input/barcode_scanner`: +``` +ls -lha /dev/input/barcode_scanner +``` +which can be used in the `device_paths` section of the **barcode-server** config. + +Source: [This](https://serverfault.com/questions/385260/bind-usb-keyboard-exclusively-to-specific-application/976557#976557) +and [That](https://stackoverflow.com/questions/63478999/how-to-make-linux-ignore-a-keyboard-while-keeping-it-available-for-my-program-to/63531743#63531743) + +# Contributing + +GitHub is for social coding: if you want to write code, I encourage contributions +through pull requests from forks of this repository. Create GitHub tickets for +bugs and new features and comment on the ones that you are interested in. + +# License + +```text +barcode-server 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 . +``` diff --git a/barcode_server.yaml b/barcode_server.yaml new file mode 100644 index 0000000..c6096cc --- /dev/null +++ b/barcode_server.yaml @@ -0,0 +1,63 @@ +barcode_server: + + # (optional) the verbosity level of log output + log_level: DEBUG + + # (optional) an identifier for this barcode-server instance + # if omitted, this will be a UUIDv4 + id: cash-register-1 + + # (optional) server configuration + server: + # (optional) the IP address to listen on for incoming connections + host: "127.0.0.1" + # (optional) the Port to listen on + port: 9654 + # (optional) API-Token which has to be provided by connecting clients + api_token: "EmUSqjXGfnQwn5wn6CpzJRZgoazMTRbMNgH7CXwkQG7Ph7stex" + + # (optional) Time period to retry delivering failed queued events before giving up and dropping the event + drop_event_queue_after: 2h + # (optional) Time to wait between retries + retry_interval: 2s + + # (optional) HTTP push configuration + http: + # URL to send events to using a request + url: "http://dummy.restapiexample.com/api/v1/create" + # The request method to use + method: POST + # Headers to set on each request + headers: + - "X-Auth-Token: MY_HEADERS" + + # (optional) MQTT push configuration + mqtt: + # MQTT server host address + host: "mqtt.mydomain.com" + # (optional) MQTT server port + port: 1883 + # (optional) Client ID of this barcode-server instance to provide to the MQTT server + client_id: "barcode-server" + # MQTT topic to push events to + topic: "barcode-server/barcode" + # Username to use when connecting to the MQTT server + user: "myuser" + # Password to use when connecting to the MQTT server + password: "mypassword" + # (optional) QoS value of event messages + qos: 2 + # (optional) Whether to instruct the MQTT server to remember event messages between restarts (of the MQTT server) + retain: True + + # A list of regex patterns to match USB device names against + devices: + - ".*Barcode.*" + # A list of absolute file paths to devices + device_paths: + #- "/dev/input/barcode_scanner" + + # (optional) Statistics configuration + stats: + # (optional) port to provide statistics on + port: 8000 diff --git a/barcode_server/__init__.py b/barcode_server/__init__.py new file mode 100644 index 0000000..55e4709 --- /dev/null +++ b/barcode_server/__init__.py @@ -0,0 +1 @@ +__version__ = "2.3.0" diff --git a/barcode_server/barcode.py b/barcode_server/barcode.py new file mode 100644 index 0000000..60d88c5 --- /dev/null +++ b/barcode_server/barcode.py @@ -0,0 +1,170 @@ +import asyncio +import logging +import uuid +from datetime import datetime +from pathlib import Path +from typing import List, Dict + +import evdev +from evdev import * + +from barcode_server.config import AppConfig +from barcode_server.keyevent_reader import KeyEventReader +from barcode_server.stats import SCAN_COUNT, DEVICES_COUNT, DEVICE_DETECTION_TIME + +LOGGER = logging.getLogger(__name__) + + +class BarcodeEvent: + + def __init__(self, input_device: InputDevice, barcode: str, date: datetime = None): + self.id = str(uuid.uuid4()) + self.date = date if date is not None else datetime.now() + self.device = input_device + self.input_device = self.device + self.barcode = barcode + + +class BarcodeReader: + """ + Reads barcodes from all USB barcode scanners in the system + """ + + def __init__(self, config: AppConfig): + self.config = config + self.devices = {} + self.listeners = set() + + self._keyevent_reader = KeyEventReader() + + self._main_task = None + self._device_tasks = {} + + async def start(self): + """ + Start detecting and reading barcode scanner devices + """ + self._main_task = asyncio.create_task(self._detect_and_read()) + + async def stop(self): + """ + Stop detecting and reading barcode scanner devices + """ + if self._main_task is None: + return + + for device_path, t in self._device_tasks.items(): + t.cancel() + self._device_tasks.clear() + self._main_task.cancel() + self._main_task = None + + async def _detect_and_read(self): + """ + Detect barcode scanner devices and start readers for them + """ + while True: + try: + self.devices = self._find_devices(self.config.DEVICE_PATTERNS.value, self.config.DEVICE_PATHS.value) + DEVICES_COUNT.set(len(self.devices)) + + for path, d in self.devices.items(): + if path in self._device_tasks: + continue + LOGGER.info( + f"Reading: {d.path}: Name: {d.name}, " + f"Vendor: {d.info.vendor:04x}, Product: {d.info.product:04x}") + task = asyncio.create_task(self._start_reader(d)) + self._device_tasks[path] = task + + await asyncio.sleep(1) + except Exception as e: + logging.exception(e) + await asyncio.sleep(10) + + async def _start_reader(self, input_device): + """ + Start a reader for a specific device + :param input_device: the input device + """ + try: + # become the sole recipient of all incoming input events + input_device.grab() + while True: + barcode = await self._read_line(input_device) + if barcode is not None and len(barcode) > 0: + event = BarcodeEvent(input_device, barcode) + asyncio.create_task(self._notify_listeners(event)) + except Exception as e: + LOGGER.exception(e) + self._device_tasks.pop(input_device.path) + finally: + try: + # release device + input_device.ungrab() + except Exception as e: + pass + + @staticmethod + @DEVICE_DETECTION_TIME.time() + def _find_devices(patterns: List, paths: List[str]) -> Dict[str, InputDevice]: + """ + # Finds the input device with the name ".*Barcode Reader.*". + # Could and should be parameterized, of course. Device name as cmd line parameter, perhaps? + :param patterns: list of patterns to match the device name against + :return: Map of ("Device Path" -> InputDevice) items + """ + result = {} + # find devices + devices = evdev.list_devices() + # create InputDevice instances + devices = [evdev.InputDevice(fn) for fn in devices] + + # filter by device name + devices = list(filter(lambda d: any(map(lambda y: y.match(d.name), patterns)), devices)) + + # add manually defined paths + for path in paths: + try: + if Path(path).exists(): + devices.append(evdev.InputDevice(path)) + else: + logging.warning(f"Path doesn't exist: {path}") + except Exception as e: + logging.exception(e) + + for d in devices: + result[d.path] = d + + return result + + async def _read_line(self, input_device: InputDevice) -> str or None: + """ + Read a single line (ENTER stops input) from the given device + :param input_device: the device to listen on + :return: a barcode + """ + # Using a thread executor here is a workaround for + # input_device.async_read_loop() skipping input events sometimes, + # so we use a synchronous method instead. + # While not perfect, it has a much higher success rate. + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, self._keyevent_reader.read_line, input_device) + return result + + def add_listener(self, listener: callable): + """ + Add a barcode event listener + :param listener: async callable taking two arguments + """ + self.listeners.add(listener) + + async def _notify_listeners(self, event: BarcodeEvent): + """ + Notifies all listeners about the scanned barcode + :param event: barcode event + """ + SCAN_COUNT.inc() + LOGGER.info(f"{event.input_device.name} ({event.input_device.path}): {event.barcode}") + for listener in self.listeners: + asyncio.create_task(listener(event)) diff --git a/barcode_server/cli.py b/barcode_server/cli.py new file mode 100644 index 0000000..3c7f2f4 --- /dev/null +++ b/barcode_server/cli.py @@ -0,0 +1,94 @@ +import asyncio +import logging +import os +import signal +import sys + +import click +from container_app_conf.formatter.toml import TomlFormatter +from prometheus_client import start_http_server + +parent_dir = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..")) +sys.path.append(parent_dir) + +logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +LOGGER = logging.getLogger(__name__) + +loop = asyncio.get_event_loop() + + +def signal_handler(signal=None, frame=None): + LOGGER.info("Exiting...") + os._exit(0) + + +CMD_OPTION_NAMES = { +} + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.version_option() +def cli(): + pass + + +def get_option_names(parameter: str) -> list: + """ + Returns a list of all valid console parameter names for a given parameter + :param parameter: the parameter to check + :return: a list of all valid names to use this parameter + """ + return CMD_OPTION_NAMES[parameter] + + +@cli.command(name="run") +def c_run(): + """ + Run the barcode-server + """ + from barcode_server.barcode import BarcodeReader + from barcode_server.config import AppConfig + from barcode_server.webserver import Webserver + + signal.signal(signal.SIGINT, signal_handler) + + config = AppConfig() + + log_level = logging._nameToLevel.get(str(config.LOG_LEVEL.value).upper(), config.LOG_LEVEL.default) + LOGGER = logging.getLogger("barcode_server") + LOGGER.setLevel(log_level) + + LOGGER.info("=== barcode-server ===") + LOGGER.info(f"Instance ID: {config.INSTANCE_ID.value}") + + barcode_reader = BarcodeReader(config) + webserver = Webserver(config, barcode_reader) + + # start prometheus server + if config.STATS_PORT.value is not None: + LOGGER.info("Starting statistics webserver...") + start_http_server(config.STATS_PORT.value) + + tasks = asyncio.gather( + webserver.start(), + ) + + loop.run_until_complete(tasks) + loop.run_forever() + + +@cli.command(name="config") +def c_config(): + """ + Print the current configuration of barcode-server + """ + from barcode_server.config import AppConfig + + config = AppConfig() + click.echo(config.print(TomlFormatter())) + + +if __name__ == '__main__': + cli() diff --git a/barcode_server/config.py b/barcode_server/config.py new file mode 100644 index 0000000..279017a --- /dev/null +++ b/barcode_server/config.py @@ -0,0 +1,250 @@ +# Copyright (c) 2019 Markus Ressel +# +# 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 . +import logging +import re +import uuid + +from container_app_conf import ConfigBase +from container_app_conf.entry.bool import BoolConfigEntry +from container_app_conf.entry.file import FileConfigEntry +from container_app_conf.entry.int import IntConfigEntry +from container_app_conf.entry.list import ListConfigEntry +from container_app_conf.entry.regex import RegexConfigEntry +from container_app_conf.entry.string import StringConfigEntry +from container_app_conf.entry.timedelta import TimeDeltaConfigEntry +from container_app_conf.source.env_source import EnvSource +from container_app_conf.source.toml_source import TomlSource +from container_app_conf.source.yaml_source import YamlSource +from py_range_parse import Range + +from barcode_server.const import * + + +class AppConfig(ConfigBase): + + def __new__(cls, *args, **kwargs): + yaml_source = YamlSource(CONFIG_NODE_ROOT) + toml_source = TomlSource(CONFIG_NODE_ROOT) + data_sources = [ + EnvSource(), + yaml_source, + toml_source, + ] + return super(AppConfig, cls).__new__(cls, data_sources=data_sources) + + LOG_LEVEL = StringConfigEntry( + description="Log level", + key_path=[ + CONFIG_NODE_ROOT, + "log_level" + ], + regex=re.compile(f" {'|'.join(logging._nameToLevel.keys())}", flags=re.IGNORECASE), + default="INFO", + ) + + INSTANCE_ID = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + "id" + ], + regex=re.compile("[0-9a-zA-Z\.\_\-\+\/\#]+"), + default=str(uuid.uuid4()), + required=True + ) + + SERVER_HOST = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_SERVER, + "host" + ], + default=DEFAULT_SERVER_HOST, + secret=True) + + SERVER_PORT = IntConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_SERVER, + CONFIG_NODE_PORT + ], + range=Range(1, 65534), + default=DEFAULT_SERVER_PORT) + + SERVER_API_TOKEN = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_SERVER, + "api_token" + ], + default=None, + secret=True + ) + + DROP_EVENT_QUEUE_AFTER = TimeDeltaConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + "drop_event_queue_after" + ], + default="2h", + ) + + RETRY_INTERVAL = TimeDeltaConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + "retry_interval" + ], + default="2s", + ) + + HTTP_METHOD = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_HTTP, + "method" + ], + required=True, + default="POST", + regex="GET|POST|PUT|PATCH" + ) + + HTTP_URL = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_HTTP, + "url" + ], + required=False + ) + + HTTP_HEADERS = ListConfigEntry( + item_type=StringConfigEntry, + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_HTTP, + "headers" + ], + default=[] + ) + + MQTT_HOST = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "host" + ], + required=False + ) + MQTT_PORT = IntConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "port" + ], + required=True, + default=1883, + range=Range(1, 65534), + ) + + MQTT_CLIENT_ID = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "client_id" + ], + default="barcode-server" + ) + + MQTT_USER = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "user" + ] + ) + + MQTT_PASSWORD = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "password" + ], + secret=True + ) + + MQTT_TOPIC = StringConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "topic" + ], + default="barcode-server/barcode", + required=True + ) + + MQTT_QOS = IntConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "qos" + ], + default=2, + required=True + ) + + MQTT_RETAIN = BoolConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_MQTT, + "retain" + ], + default=False, + required=True + ) + + DEVICE_PATTERNS = ListConfigEntry( + item_type=RegexConfigEntry, + item_args={ + "flags": re.IGNORECASE + }, + key_path=[ + CONFIG_NODE_ROOT, + "devices" + ], + default=[] + ) + + DEVICE_PATHS = ListConfigEntry( + item_type=FileConfigEntry, + key_path=[ + CONFIG_NODE_ROOT, + "device_paths" + ], + default=[] + ) + + STATS_PORT = IntConfigEntry( + key_path=[ + CONFIG_NODE_ROOT, + CONFIG_NODE_STATS, + CONFIG_NODE_PORT + ], + default=8000, + required=False + ) + + def validate(self): + super(AppConfig, self).validate() + if len(self.DEVICE_PATHS.value) == len(self.DEVICE_PATTERNS.value) == 0: + raise AssertionError("You must provide at least one device pattern or device_path!") diff --git a/barcode_server/const.py b/barcode_server/const.py new file mode 100644 index 0000000..7145061 --- /dev/null +++ b/barcode_server/const.py @@ -0,0 +1,32 @@ +# Copyright (c) 2019 Markus Ressel +# +# 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 . + +Client_Id = "Client-ID" +Drop_Event_Queue = "Drop-Event-Queue" +X_Auth_Token = "X-Auth-Token" + +CONFIG_NODE_ROOT = "barcode_server" + +CONFIG_NODE_SERVER = "server" +CONFIG_NODE_HTTP = "http" +CONFIG_NODE_MQTT = "mqtt" + +CONFIG_NODE_STATS = "stats" +CONFIG_NODE_PORT = "port" + +DEFAULT_SERVER_HOST = "127.0.0.1" +DEFAULT_SERVER_PORT = 9654 + +ENDPOINT_DEVICES = "devices" diff --git a/barcode_server/keyevent_reader.py b/barcode_server/keyevent_reader.py new file mode 100644 index 0000000..bcebfa6 --- /dev/null +++ b/barcode_server/keyevent_reader.py @@ -0,0 +1,198 @@ +import logging + +from evdev import KeyEvent, InputDevice, categorize + +LOGGER = logging.getLogger(__name__) + +US_EN_UPPER_DICT = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + ",": "<", + ".": ">", + "/": "?", + ";": ":", + "'": "\"", + "\\": "|", + "[": "{", + "]": "}" +} + + +class KeyEventReader: + """ + Class used to convert a sequence of KeyEvents to text + """ + + def __init__(self): + self._shift = False + self._caps = False + self._alt = False + self._unicode_number_input_buffer = "" + + self._line = "" + + def read_line(self, input_device: InputDevice) -> str: + """ + Reads a line + :param input_device: the device to read from + :return: line + """ + self._line = "" + # While there is a function called async_read_loop, it tends + # to skip input events, so we use the non-async read-loop here. + + # async for event in input_device.async_read_loop(): + for event in input_device.read_loop(): + try: + event = categorize(event) + + if hasattr(event, "event"): + if not hasattr(event, "keystate") and hasattr(event.event, "keystate"): + event.keystate = event.event.keystate + + if not hasattr(event, "keystate") or not hasattr(event, "keycode"): + continue + + keycode = event.keycode + keystate = event.keystate + + if isinstance(event, KeyEvent): + if self._on_key_event(keycode, keystate): + return self._line + elif hasattr(event, "event") and event.event.type == 1: + if self._on_key_event(keycode, keystate): + return self._line + except Exception as ex: + LOGGER.exception(ex) + + def _on_key_event(self, code: str, state: int) -> bool: + if code in ["KEY_ENTER", "KEY_KPENTER"]: + if state == KeyEvent.key_up: + # line is finished + self._reset_modifiers() + return True + elif code in ["KEY_RIGHTSHIFT", "KEY_LEFTSHIFT"]: + if state in [KeyEvent.key_down, KeyEvent.key_hold]: + self._shift = True + else: + self._shift = False + elif code in ["KEY_LEFTALT", "KEY_RIGHTALT"]: + if state in [KeyEvent.key_down, KeyEvent.key_hold]: + self._alt = True + else: + self._alt = False + + character = self._unicode_numbers_to_character(self._unicode_number_input_buffer) + self._unicode_number_input_buffer = "" + + if character is not None: + self._line += character + + elif code == "KEY_BACKSPACE": + self._line = self._line[:-1] + elif state == KeyEvent.key_down: + character = self._code_to_character(code) + if self._alt: + self._unicode_number_input_buffer += character + else: + if character is not None and not self._alt: + # append the current character + self._line += character + + return False + + def _code_to_character(self, code: str) -> chr or None: + character = None + + if len(code) == 5: + character = code[-1] + elif code.startswith("KEY_KP") and len(code) == 7: + character = code[-1] + + elif code in ["KEY_DOWN"]: + character = '\n' + elif code in ["KEY_SPACE"]: + character = ' ' + elif code in ["KEY_ASTERISK", "KEY_KPASTERISK"]: + character = '*' + elif code in ["KEY_MINUS", "KEY_KPMINUS"]: + character = '-' + elif code in ["KEY_PLUS", "KEY_KPPLUS"]: + character = '+' + elif code in ["KEY_QUESTION"]: + character = '?' + elif code in ["KEY_COMMA", "KEY_KPCOMMA"]: + character = ',' + elif code in ["KEY_DOT", "KEY_KPDOT"]: + character = '.' + elif code in ["KEY_EQUAL", "KEY_KPEQUAL"]: + character = '=' + elif code in ["KEY_LEFTPAREN", "KEY_KPLEFTPAREN"]: + character = '(' + elif code in ["KEY_PLUSMINUS", "KEY_KPPLUSMINUS"]: + character = '+-' + elif code in ["KEY_RIGHTPAREN", "KEY_KPRIGHTPAREN"]: + character = ')' + elif code in ["KEY_RIGHTBRACE"]: + character = ']' + elif code in ["KEY_LEFTBRACE"]: + character = '[' + elif code in ["KEY_SLASH", "KEY_KPSLASH"]: + character = '/' + elif code in ["KEY_BACKSLASH"]: + character = '\\' + elif code in ["KEY_COLON"]: + character = ';' + elif code in ["KEY_SEMICOLON"]: + character = ';' + elif code in ["KEY_APOSTROPHE"]: + character = '\'' + elif code in ["KEY_GRAVE"]: + character = '`' + + if character is None: + character = code[4:] + if len(character) > 1: + LOGGER.warning(f"Unhandled Keycode: {code}") + + if self._shift or self._caps: + character = character.upper() + if character in US_EN_UPPER_DICT.keys(): + character = US_EN_UPPER_DICT[character] + else: + character = character.lower() + + return character + + @staticmethod + def _unicode_numbers_to_character(code: str) -> chr or None: + if code is None or len(code) <= 0: + return None + + try: + # convert to hex + i = int(code) + h = hex(i) + s = f"{h}" + + return bytearray.fromhex(s[2:]).decode('utf-8') + except Exception as ex: + LOGGER.exception(ex) + return None + + def _reset_modifiers(self): + self._alt = False + self._unicode_number_input_buffer = "" + self._shift = False + self._caps = False diff --git a/barcode_server/notifier/__init__.py b/barcode_server/notifier/__init__.py new file mode 100644 index 0000000..3d686bf --- /dev/null +++ b/barcode_server/notifier/__init__.py @@ -0,0 +1,103 @@ +import asyncio +import logging +from asyncio import Task, QueueEmpty +from datetime import datetime +from typing import Optional + +from barcode_server.barcode import BarcodeEvent +from barcode_server.config import AppConfig + +LOGGER = logging.getLogger(__name__) + + +class BarcodeNotifier: + """ + Base class for a notifier. + """ + + def __init__(self): + self.config = AppConfig() + self.drop_event_queue_after = self.config.DROP_EVENT_QUEUE_AFTER.value + self.retry_interval = self.config.RETRY_INTERVAL.value + self.event_queue = asyncio.Queue() + self.processor_task: Optional[Task] = None + + def is_running(self) -> bool: + return self.processor_task is not None + + async def start(self): + """ + Starts the event processor of this notifier + """ + self.processor_task = asyncio.create_task(self.event_processor()) + + async def stop(self): + """ + Stops the event processor of this notifier + """ + if self.processor_task is None: + return + + self.processor_task.cancel() + self.processor_task = None + + async def drop_queue(self): + """ + Drops all items in the event queue + """ + running = self.is_running() + # stop if currently running + if running: + await self.stop() + + # mark all items as finished + for _ in range(self.event_queue.qsize()): + try: + self.event_queue.get_nowait() + self.event_queue.task_done() + except QueueEmpty as ex: + break + + # restart if it was running + if running: + await self.start() + + async def event_processor(self): + """ + Processes the event queue + """ + while True: + try: + event = await self.event_queue.get() + + success = False + while not success: + if datetime.now() - event.date >= self.drop_event_queue_after: + # event is older than threshold, so we just skip it + self.event_queue.task_done() + break + + try: + await self._send_event(event) + success = True + self.event_queue.task_done() + except Exception as ex: + LOGGER.exception(ex) + await asyncio.sleep(self.retry_interval.total_seconds()) + + except Exception as ex: + LOGGER.exception(ex) + + async def add_event(self, event: BarcodeEvent): + """ + Adds an event to the event queue + """ + await self.event_queue.put(event) + + async def _send_event(self, event: BarcodeEvent): + """ + Sends the given event to the notification target + :param event: barcode event + """ + raise NotImplementedError() + diff --git a/barcode_server/notifier/http.py b/barcode_server/notifier/http.py new file mode 100644 index 0000000..9dfa07b --- /dev/null +++ b/barcode_server/notifier/http.py @@ -0,0 +1,30 @@ +import logging +from typing import List + +import aiohttp +from prometheus_async.aio import time + +from barcode_server.barcode import BarcodeEvent +from barcode_server.notifier import BarcodeNotifier +from barcode_server.stats import HTTP_NOTIFIER_TIME +from barcode_server.util import barcode_event_to_json + +LOGGER = logging.getLogger(__name__) + + +class HttpNotifier(BarcodeNotifier): + + def __init__(self, method: str, url: str, headers: List[str]): + super().__init__() + self.method = method + self.url = url + headers = list(map(lambda x: tuple(x.split(':', 1)), headers)) + self.headers = list(map(lambda x: (x[0].strip(), x[1].strip()), headers)) + + @time(HTTP_NOTIFIER_TIME) + async def _send_event(self, event: BarcodeEvent): + json = barcode_event_to_json(self.config.INSTANCE_ID.value, event) + async with aiohttp.ClientSession() as session: + async with session.request(self.method, self.url, headers=self.headers, data=json) as resp: + resp.raise_for_status() + LOGGER.debug(f"Notified {self.url}: {event.barcode}") diff --git a/barcode_server/notifier/mqtt.py b/barcode_server/notifier/mqtt.py new file mode 100644 index 0000000..9d09132 --- /dev/null +++ b/barcode_server/notifier/mqtt.py @@ -0,0 +1,38 @@ +import logging + +from asyncio_mqtt import Client +from prometheus_async.aio import time + +from barcode_server.barcode import BarcodeEvent +from barcode_server.notifier import BarcodeNotifier +from barcode_server.stats import MQTT_NOTIFIER_TIME +from barcode_server.util import barcode_event_to_json + +LOGGER = logging.getLogger(__name__) + + +class MQTTNotifier(BarcodeNotifier): + + def __init__(self, host: str, port: int = 1883, + topic: str = "/barcode-server/barcode", + client_id: str = "barcode-server", + user: str = None, password: str = None, + qos: int = 2, retain: bool = False): + super().__init__() + self.client_id = client_id + self.host = host + self.port = port + self.user = user + self.password = password + self.topic = topic + self.qos = qos + self.retain = retain + + @time(MQTT_NOTIFIER_TIME) + async def _send_event(self, event: BarcodeEvent): + json = barcode_event_to_json(self.config.INSTANCE_ID.value, event) + async with Client(hostname=self.host, port=self.port, + username=self.user, password=self.password, + client_id=self.client_id) as client: + await client.publish(self.topic, json, self.qos, self.retain) + LOGGER.debug(f"Notified {self.host}:{self.port}: {event.barcode}") diff --git a/barcode_server/notifier/ws.py b/barcode_server/notifier/ws.py new file mode 100644 index 0000000..74f2090 --- /dev/null +++ b/barcode_server/notifier/ws.py @@ -0,0 +1,23 @@ +from prometheus_async.aio import time + +from barcode_server.barcode import BarcodeEvent +from barcode_server.notifier import BarcodeNotifier +from barcode_server.stats import WEBSOCKET_NOTIFIER_TIME +from barcode_server.util import barcode_event_to_json + + +class WebsocketNotifier(BarcodeNotifier): + + def __init__(self, websocket): + super().__init__() + self.websocket = websocket + + @time(WEBSOCKET_NOTIFIER_TIME) + async def _send_event(self, event: BarcodeEvent): + json = barcode_event_to_json(self.config.INSTANCE_ID.value, event) + await self.websocket.send_bytes(json) + + # TODO: cant log websocket address here because we don't have access + # to an unique identifier anymore, maybe we need to store one manually + # when the websocket is connected initially... + # LOGGER.debug(f"Notified {client.remote_address}") diff --git a/barcode_server/stats.py b/barcode_server/stats.py new file mode 100644 index 0000000..8ac6474 --- /dev/null +++ b/barcode_server/stats.py @@ -0,0 +1,28 @@ +from prometheus_client import Gauge, Summary + +from barcode_server.const import * + +WEBSOCKET_CLIENT_COUNT = Gauge( + 'websocket_client_count', + 'Number of currently connected websocket clients' +) + +DEVICES_COUNT = Gauge( + 'devices_count', + 'Number of currently detected devices' +) + +SCAN_COUNT = Gauge( + 'scan_count', + 'Number of times a scan has been detected' +) + +DEVICE_DETECTION_TIME = Summary('device_detection_processing_seconds', 'Time spent detecting devices') + +REST_TIME = Summary('rest_endpoint_processing_seconds', 'Time spent in a rest command handler', ['endpoint']) +REST_TIME_DEVICES = REST_TIME.labels(endpoint=ENDPOINT_DEVICES) + +NOTIFIER_TIME = Summary('notifier_processing_seconds', 'Time spent in a notifier', ['type']) +WEBSOCKET_NOTIFIER_TIME = NOTIFIER_TIME.labels(type='websocket') +HTTP_NOTIFIER_TIME = NOTIFIER_TIME.labels(type='http') +MQTT_NOTIFIER_TIME = NOTIFIER_TIME.labels(type='mqtt') diff --git a/barcode_server/util.py b/barcode_server/util.py new file mode 100644 index 0000000..62d3fd5 --- /dev/null +++ b/barcode_server/util.py @@ -0,0 +1,38 @@ +from evdev import InputDevice + +from barcode_server.barcode import BarcodeEvent + + +def input_device_to_dict(input_device: InputDevice) -> dict: + """ + Converts an input device to a a dictionary with human readable values + :param input_device: the device to convert + :return: dictionary + """ + return { + "name": input_device.name, + "path": input_device.path, + "vendorId": f"{input_device.info.vendor: 04x}", + "productId": f"{input_device.info.product: 04x}", + } + + +def barcode_event_to_json(server_id: str, event: BarcodeEvent) -> bytes: + """ + Converts a barcode event to json + :param server_id: server instance id + :param event: the event to convert + :return: json representation + """ + import orjson + + event = { + "id": event.id, + "serverId": server_id, + "date": event.date.isoformat(), + "device": input_device_to_dict(event.input_device), + "barcode": event.barcode + } + + json = orjson.dumps(event) + return json diff --git a/barcode_server/webserver.py b/barcode_server/webserver.py new file mode 100644 index 0000000..737a790 --- /dev/null +++ b/barcode_server/webserver.py @@ -0,0 +1,180 @@ +import asyncio +import logging +from typing import Dict + +import aiohttp +from aiohttp import web +from aiohttp.web_middlewares import middleware +from prometheus_async.aio import time + +from barcode_server.barcode import BarcodeReader, BarcodeEvent +from barcode_server.config import AppConfig +from barcode_server.const import * +from barcode_server.notifier import BarcodeNotifier +from barcode_server.notifier.http import HttpNotifier +from barcode_server.notifier.mqtt import MQTTNotifier +from barcode_server.notifier.ws import WebsocketNotifier +from barcode_server.stats import REST_TIME_DEVICES, WEBSOCKET_CLIENT_COUNT +from barcode_server.util import input_device_to_dict + +LOGGER = logging.getLogger(__name__) +routes = web.RouteTableDef() + + +class Webserver: + + def __init__(self, config: AppConfig, barcode_reader: BarcodeReader): + self.config = config + self.host = config.SERVER_HOST.value + self.port = config.SERVER_PORT.value + + self.clients = {} + + self.barcode_reader = barcode_reader + self.barcode_reader.add_listener(self.on_barcode) + + self.notifiers: Dict[str, BarcodeNotifier] = {} + if config.HTTP_URL.value is not None: + http_notifier = HttpNotifier( + config.HTTP_METHOD.value, + config.HTTP_URL.value, + config.HTTP_HEADERS.value) + self.notifiers["http"] = http_notifier + + if config.MQTT_HOST.value is not None: + mqtt_notifier = MQTTNotifier( + host=config.MQTT_HOST.value, + port=config.MQTT_PORT.value, + client_id=config.MQTT_CLIENT_ID.value, + user=config.MQTT_USER.value, + password=config.MQTT_PASSWORD.value, + topic=config.MQTT_TOPIC.value, + qos=config.MQTT_QOS.value, + retain=config.MQTT_RETAIN.value, + ) + self.notifiers["mqtt"] = mqtt_notifier + + async def start(self): + # start detecting and reading barcode scanners + await self.barcode_reader.start() + # start notifier queue processors + for key, notifier in self.notifiers.items(): + LOGGER.debug(f"Starting notifier: {key}") + await notifier.start() + LOGGER.info(f"Starting webserver on {self.config.SERVER_HOST.value}:{self.config.SERVER_PORT.value} ...") + + app = self.create_app() + runner = aiohttp.web.AppRunner(app) + await runner.setup() + site = aiohttp.web.TCPSite( + runner, + host=self.config.SERVER_HOST.value, + port=self.config.SERVER_PORT.value + ) + await site.start() + + # wait forever + return await asyncio.Event().wait() + + def create_app(self) -> web.Application: + app = web.Application(middlewares=[self.authentication_middleware]) + app.add_routes(routes) + return app + + @middleware + async def authentication_middleware(self, request, handler): + if self.config.SERVER_API_TOKEN.value is not None and \ + (X_Auth_Token not in request.headers.keys() + or request.headers[X_Auth_Token] != self.config.SERVER_API_TOKEN.value): + LOGGER.warning(f"Rejecting unauthorized connection: {request.host}") + return web.HTTPUnauthorized() + + if Client_Id not in request.headers.keys(): + LOGGER.warning(f"Rejecting client without {Client_Id} header: {request.host}") + return web.HTTPBadRequest() + + client_id = request.headers[Client_Id].lower().strip() + + if self.clients.get(client_id, None) is not None: + LOGGER.warning( + f"Rejecting new connection of already connected client {request.headers[Client_Id]}: {request.host}") + return web.HTTPBadRequest() + + return await handler(self, request) + + @routes.get(f"/{ENDPOINT_DEVICES}") + @time(REST_TIME_DEVICES) + async def devices_handle(self, request): + import orjson + device_list = list(map(input_device_to_dict, self.barcode_reader.devices.values())) + json = orjson.dumps(device_list) + return web.Response(body=json, content_type="application/json") + + @routes.get("/") + async def websocket_handler(self, request): + client_id = request.headers[Client_Id].lower().strip() + + websocket = web.WebSocketResponse() + await websocket.prepare(request) + + self.clients[client_id] = websocket + active_client_count = self.count_active_clients() + known_client_ids_count = len(self.clients.keys()) + # TODO: report both the mount of currently connected clients, as well as known client ids + WEBSOCKET_CLIENT_COUNT.set(active_client_count) + + if client_id not in self.notifiers.keys(): + LOGGER.debug( + f"New client connected: {client_id} (from {request.host})") + + LOGGER.debug(f"Creating new notifier for client id: {client_id}") + notifier = WebsocketNotifier(websocket) + self.notifiers[client_id] = notifier + else: + LOGGER.debug( + f"Previously seen client reconnected: {client_id} (from {request.host})") + + notifier = self.notifiers[client_id] + if isinstance(notifier, WebsocketNotifier): + notifier.websocket = websocket + + if Drop_Event_Queue in request.headers.keys(): + await notifier.drop_queue() + + LOGGER.debug(f"Starting notifier: {client_id}") + await notifier.start() + + try: + async for msg in websocket: + if msg.type == aiohttp.WSMsgType.TEXT: + if msg.data.strip() == 'close': + await websocket.close() + else: + await websocket.send_str(msg.data + '/answer') + elif msg.type == aiohttp.WSMsgType.ERROR: + LOGGER.debug('ws connection closed with exception %s' % + websocket.exception()) + except Exception as e: + LOGGER.exception(e) + finally: + # TODO: should we remove this notifier after some time? + LOGGER.debug(f"Stopping notifier: {client_id}") + await notifier.stop() + + self.clients[client_id] = None + self.clients.pop(client_id) + active_client_count = self.count_active_clients() + WEBSOCKET_CLIENT_COUNT.set(active_client_count) + LOGGER.debug(f"Client disconnected: {client_id} (from {request.host})") + return websocket + + async def on_barcode(self, event: BarcodeEvent): + for key, notifier in self.notifiers.items(): + await notifier.add_event(event) + + def count_active_clients(self): + """ + Counts the number of clients with an active websocket connection + :return: number of active clients + """ + return len(list(filter(lambda x: x[1] is not None, self.clients.items()))) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..96a44b9 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +set -eu +sudo -E -u "#${PUID}" -g "#${PGID}" "$@" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..427e1c5 --- /dev/null +++ b/setup.py @@ -0,0 +1,72 @@ +import os + +from setuptools import setup, find_packages + +DEVELOPMENT_STATUS = "Development Status :: 5 - Production/Stable" + + +def read_version(package): + with open(os.path.join(package, '__init__.py'), 'r') as fd: + for line in fd: + if line.startswith('__version__ = '): + return line.split()[-1].strip().strip("'").strip("\"") + + +def readme_type() -> str: + import os + if os.path.exists("README.rst"): + return "text/x-rst" + if os.path.exists("README.md"): + return "text/markdown" + + +def readme() -> [str]: + with open('README.md') as f: + return f.read() + + +def locked_requirements(section): + """ + Look through the 'Pipfile.lock' to fetch requirements by section. + """ + import json + + with open('Pipfile.lock') as pip_file: + pipfile_json = json.load(pip_file) + + if section not in pipfile_json: + print("{0} section missing from Pipfile.lock".format(section)) + return [] + + return [package + detail.get('version', "") + for package, detail in pipfile_json[section].items()] + + +setup( + name='barcode-server', + version=read_version("barcode_server"), + description='A simple daemon to expose USB Barcode Scanner data on the network.', + long_description=readme(), + long_description_content_type=readme_type(), + license='GPLv3+', + author='Markus Ressel', + author_email='mail@markusressel.de', + url='https://github.com/markusressel/barcode-server', + packages=find_packages(), + # python_requires='>=3.4', + classifiers=[ + DEVELOPMENT_STATUS, + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9' + ], + install_requires=locked_requirements('default'), + tests_require=locked_requirements('develop'), + entry_points={ + 'console_scripts': [ + 'barcode-server = barcode_server.cli:cli' + ] + } +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..bbc4c29 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,34 @@ +# Copyright (c) 2019 Markus Ressel +# . +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# . +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# . +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from unittest import IsolatedAsyncioTestCase + + +class TestBase(IsolatedAsyncioTestCase): + from barcode_server.config import AppConfig + from container_app_conf.source.yaml_source import YamlSource + + # load config from test folder + config = AppConfig( + singleton=True, + data_sources=[ + YamlSource("barcode_server", "./tests/") + ] + ) diff --git a/tests/api_test.py b/tests/api_test.py new file mode 100644 index 0000000..375919c --- /dev/null +++ b/tests/api_test.py @@ -0,0 +1,32 @@ +from datetime import datetime +from unittest.mock import Mock + +from barcode_server.barcode import BarcodeEvent +from barcode_server.util import barcode_event_to_json +from tests import TestBase + + +class ApiTest(TestBase): + + def test_json(self): + date_str = "2020-08-03T10:00:00+00:00" + + input_device = Mock() + input_device.name = "Barcode Scanner" + input_device.path = "/dev/input/event2" + input_device.info.vendor = 1 + input_device.info.product = 2 + + date = datetime.fromisoformat(str(date_str)) + barcode = "4006824000970" + + server_id = "server-id" + event = BarcodeEvent(input_device, barcode, date) + event_json = str(barcode_event_to_json(server_id, event)) + + self.assertIn(event.id, event_json) + self.assertIn(server_id, event_json) + self.assertIn(date_str, event_json) + self.assertIn(input_device.path, event_json) + self.assertIn(barcode, event_json) + self.assertIsNotNone(event_json) diff --git a/tests/barcode_reader_test.py b/tests/barcode_reader_test.py new file mode 100644 index 0000000..ff45663 --- /dev/null +++ b/tests/barcode_reader_test.py @@ -0,0 +1,11 @@ +from barcode_server.barcode import BarcodeReader +from barcode_server.config import AppConfig +from tests import TestBase + + +class BarcodeReaderTest(TestBase): + + async def test_initialization(self): + config = AppConfig() + reader = BarcodeReader(config) + self.assertIsNotNone(reader) diff --git a/tests/barcode_server.yaml b/tests/barcode_server.yaml new file mode 100644 index 0000000..9d973d7 --- /dev/null +++ b/tests/barcode_server.yaml @@ -0,0 +1,9 @@ +barcode_server: + server: + host: "127.0.0.1" + port: 9654 + api_token: "EmUSqjXGfnQwn5wn6CpzJRZgoazMTRbMNgH7CXwkQG7Ph7stex" + devices: + - "Barcode/i" + stats: + port: diff --git a/tests/key_event_reader_test.py b/tests/key_event_reader_test.py new file mode 100644 index 0000000..7c6bb02 --- /dev/null +++ b/tests/key_event_reader_test.py @@ -0,0 +1,167 @@ +from typing import List +from unittest.mock import Mock + +from evdev import InputEvent, ecodes, KeyEvent + +from barcode_server.keyevent_reader import KeyEventReader +from tests import TestBase + + +class KeyEventReaderTest(TestBase): + + @staticmethod + def fake_input_loop(input: List[InputEvent]): + input_events = input + + def read_loop(): + for event in input_events: + yield event + + return read_loop + + @staticmethod + def mock_input_event(keycode, keystate) -> InputEvent: + input_event = Mock(spec=InputEvent) + input_event.type = 1 + input_event.keystate = keystate # 0: UP, 1: Down, 2: Hold + input_event.keycode = keycode + # inverse lookup of the event code in the target structure + code = next(key for key, value in ecodes.keys.items() if value == keycode) + input_event.code = code + + return input_event + + def generate_input_event_sequence(self, expected: str, finish_line: bool = True) -> List[InputEvent]: + events = [] + keycodes = list(map(lambda x: self.character_to_keycode(x), expected)) + + for item in keycodes: + for keystate in [KeyEvent.key_down, KeyEvent.key_up]: + event = self.mock_input_event(keycode=item, keystate=keystate) + events.append(event) + + if finish_line: + for keystate in [KeyEvent.key_down, KeyEvent.key_up]: + event = self.mock_input_event(keycode="KEY_ENTER", keystate=keystate) + events.append(event) + + return events + + @staticmethod + def character_to_keycode(character: str) -> str: + char_to_keycode_map = { + '0': "KEY_KP0", + '1': "KEY_KP1", + '2': "KEY_KP2", + '3': "KEY_KP3", + '4': "KEY_KP4", + '5': "KEY_KP5", + '6': "KEY_KP6", + '7': "KEY_KP7", + '8': "KEY_KP8", + '9': "KEY_KP9", + + '*': "KEY_KPASTERISK", + '/': "KEY_SLASH", + '-': "KEY_KPMINUS", + '+': "KEY_KPPLUS", + '.': "KEY_DOT", + ',': "KEY_COMMA", + '?': "KEY_QUESTION", + + '\n': "KEY_ENTER", + } + return char_to_keycode_map[character] + + def test_mock_gen(self): + # GIVEN + expected = [ + self.mock_input_event(keycode="KEY_KPMINUS", keystate=KeyEvent.key_down), + self.mock_input_event(keycode="KEY_KPMINUS", keystate=KeyEvent.key_up), + + self.mock_input_event(keycode="KEY_DOT", keystate=KeyEvent.key_down), + self.mock_input_event(keycode="KEY_DOT", keystate=KeyEvent.key_up), + + self.mock_input_event(keycode="KEY_KPMINUS", keystate=KeyEvent.key_down), + self.mock_input_event(keycode="KEY_KPMINUS", keystate=KeyEvent.key_up), + + self.mock_input_event(keycode="KEY_ENTER", keystate=KeyEvent.key_down), + self.mock_input_event(keycode="KEY_ENTER", keystate=KeyEvent.key_up), + ] + text = "-.-" + + # WHEN + input_events = self.generate_input_event_sequence(text) + + # THEN + self.assertEqual(len(expected), len(input_events)) + for i in range(0, len(expected)): + self.assertEqual(expected[i].keycode, input_events[i].keycode) + + async def test_numbers(self): + # GIVEN + under_test = KeyEventReader() + expected = "0123456789" + input_events = self.generate_input_event_sequence(expected) + input_device = Mock() + input_device.read_loop = self.fake_input_loop(input_events) + + # WHEN + line = under_test.read_line(input_device) + + # THEN + self.assertEqual(expected, line) + + async def test_special_characters(self): + # GIVEN + under_test = KeyEventReader() + expected = "+.,*/-?" + input_events = self.generate_input_event_sequence(expected) + input_device = Mock() + input_device.read_loop = self.fake_input_loop(input_events) + + # WHEN + line = under_test.read_line(input_device) + + # THEN + self.assertEqual(expected, line) + + async def test_none_event(self): + # GIVEN + under_test = KeyEventReader() + unexpected = "0" + expected = "1" + input_events = self.generate_input_event_sequence(unexpected + expected) + input_events[0] = None + input_events[1] = None + + input_device = Mock() + input_device.read_loop = self.fake_input_loop(input_events) + + # WHEN + line = under_test.read_line(input_device) + + # THEN + self.assertEqual(expected, line) + + async def test_event_without_keystate_attribute(self): + # GIVEN + under_test = KeyEventReader() + unexpected = "0" + input_events_without_keystate = self.generate_input_event_sequence(unexpected) + for event in input_events_without_keystate: + delattr(event, "keystate") + + expected = "1" + input_events = self.generate_input_event_sequence(expected) + + input_events = input_events + input_events_without_keystate + + input_device = Mock() + input_device.read_loop = self.fake_input_loop(input_events) + + # WHEN + line = under_test.read_line(input_device) + + # THEN + self.assertEqual(expected, line) diff --git a/tests/notifier_test.py b/tests/notifier_test.py new file mode 100644 index 0000000..5fe76a0 --- /dev/null +++ b/tests/notifier_test.py @@ -0,0 +1,13 @@ +from barcode_server.notifier.http import HttpNotifier +from tests import TestBase + + +class NotifierTest(TestBase): + + def test_http(self): + method = "POST" + url = "test.de" + headers = [] + + reader = HttpNotifier(method, url, headers) + self.assertIsNotNone(reader) diff --git a/tests/websocket_notifier_test.py b/tests/websocket_notifier_test.py new file mode 100644 index 0000000..a614d9f --- /dev/null +++ b/tests/websocket_notifier_test.py @@ -0,0 +1,199 @@ +import asyncio +import random +from unittest.mock import MagicMock + +import aiohttp +from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + +from barcode_server import const +from barcode_server.barcode import BarcodeEvent +from barcode_server.util import barcode_event_to_json +from barcode_server.webserver import Webserver + + +def create_barcode_event_mock(barcode: str = None): + device = lambda: None + device.info = lambda: None + device.name = "BARCODE SCANNER BARCODE SCANNER" + device.path = "/dev/input/event3" + device.info.vendor = 1 + device.info.product = 1 + + event = BarcodeEvent( + device, + barcode if barcode is not None else f"{random.getrandbits(24)}" + ) + + return event + + +class WebsocketNotifierTest(AioHTTPTestCase): + from barcode_server.config import AppConfig + from container_app_conf.source.yaml_source import YamlSource + + # load config from test folder + config = AppConfig( + singleton=True, + data_sources=[ + YamlSource("barcode_server", "./tests/") + ] + ) + + webserver = None + + async def get_application(self): + """ + Override the get_app method to return your application. + """ + barcode_reader = MagicMock() + self.webserver = Webserver(self.config, barcode_reader) + app = self.webserver.create_app() + runner = aiohttp.web.AppRunner(app) + await runner.setup() + site = aiohttp.web.TCPSite( + runner, + host=self.config.SERVER_HOST.value, + port=self.config.SERVER_PORT.value + ) + await site.start() + return app + + # the unittest_run_loop decorator can be used in tandem with + # the AioHTTPTestCase to simplify running + # tests that are asynchronous + @unittest_run_loop + async def test_ws_connect_and_event(self): + sample_event = create_barcode_event_mock("abcdefg") + server_id = self.config.INSTANCE_ID.value + expected_json = barcode_event_to_json(server_id, sample_event) + + import uuid + client_id = str(uuid.uuid4()) + + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + 'http://127.0.0.1:9654/', + headers={ + const.Client_Id: client_id, + const.X_Auth_Token: self.config.SERVER_API_TOKEN.value or "" + }) as ws: + asyncio.create_task(self.webserver.on_barcode(sample_event)) + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + self.assertEqual(expected_json, msg.data) + await ws.close() + return + else: + self.fail("No event received") + + assert False + + @unittest_run_loop + async def test_ws_reconnect_event_catchup(self): + server_id = self.config.INSTANCE_ID.value + missed_event = create_barcode_event_mock("abcdefg") + second_event = create_barcode_event_mock("123456") + missed_event_json = barcode_event_to_json(server_id, missed_event) + second_event_json = barcode_event_to_json(server_id, second_event) + + import uuid + client_id = str(uuid.uuid4()) + + # connect to the server once + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + 'http://127.0.0.1:9654/', + headers={ + const.Client_Id: client_id, + const.X_Auth_Token: self.config.SERVER_API_TOKEN.value or "" + }) as ws: + await ws.close() + + # then emulate a barcode scan event + asyncio.create_task(self.webserver.on_barcode(missed_event)) + + await asyncio.sleep(0.1) + + # and then reconnect again, expecting the event in between + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + 'http://127.0.0.1:9654/', + headers={ + const.Client_Id: client_id, + const.X_Auth_Token: self.config.SERVER_API_TOKEN.value or "" + }) as ws: + # emulate another event, while connected + asyncio.create_task(self.webserver.on_barcode(second_event)) + + missed_event_received = False + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + if missed_event_json == msg.data: + if missed_event_received: + assert False + missed_event_received = True + elif second_event_json == msg.data: + if not missed_event_received: + assert False + await ws.close() + return + else: + assert False + else: + self.fail("No event received") + + assert False + + @unittest_run_loop + async def test_ws_reconnect_drop_queue(self): + server_id = self.config.INSTANCE_ID.value + missed_event = create_barcode_event_mock("abcdefg") + second_event = create_barcode_event_mock("123456") + missed_event_json = barcode_event_to_json(server_id, missed_event) + second_event_json = barcode_event_to_json(server_id, second_event) + + import uuid + client_id = str(uuid.uuid4()) + + # connect to the server once + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + 'http://127.0.0.1:9654/', + headers={ + const.Client_Id: client_id, + const.X_Auth_Token: self.config.SERVER_API_TOKEN.value or "" + }) as ws: + await ws.close() + + # then emulate a barcode scan event while not connected + asyncio.create_task(self.webserver.on_barcode(missed_event)) + + await asyncio.sleep(0.1) + + # and then reconnect again, passing the "drop cache" header, expecting only + # the new live event + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + 'http://127.0.0.1:9654/', + headers={ + const.Client_Id: client_id, + const.Drop_Event_Queue: "", + const.X_Auth_Token: self.config.SERVER_API_TOKEN.value or "" + }) as ws: + # emulate another event, while connected + asyncio.create_task(self.webserver.on_barcode(second_event)) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + if missed_event_json == msg.data: + self.fail("Received missed event despite queue drop") + elif second_event_json == msg.data: + await ws.close() + assert True + return + else: + self.fail("Received unexpected event") + else: + self.fail("No event received") + + assert False