diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..39dc61c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +exclude_lines = + pragma: nocover + if TYPE_CHECKING: + @abstractmethod diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..a4e324d --- /dev/null +++ b/.flake8 @@ -0,0 +1,15 @@ +[flake8] +max_line_length=120 +extend-ignore=E203 # flake8 has conflicting handling with black here +application-import-names=zerocom,tests +exclude=.venv,.git,.cache +ignore= + ANN002, # *args + ANN003, # **kwargs + ANN101, # self param annotation + ANN102, # cls param annotation + ANN204, # return type annotation for special methods + + E731, # Allow inline lambdas + MD033, # Allow inline HTML in markdown +per-file-ignores=*/__init__.py:F401,test_*.py:ANN diff --git a/.github/workflows/flake8-lint.yml b/.github/workflows/flake8-lint.yml deleted file mode 100644 index 357bab3..0000000 --- a/.github/workflows/flake8-lint.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Flake8 Linting - -on: - push: - branches: - - main - pull_request: - -jobs: - lint: - name: Flake8 Linting - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python - id: python - uses: actions/setup-python@v3 - with: - python-version: "3.9" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pipenv - pipenv sync -d - - - name: Lint with flake8 - run: | - pipenv run lint diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..8c055ec --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,85 @@ +name: Unit-Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +# Cancel already running workflows if new ones are scheduled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Make sure pip caches dependencies and installs as user + PIP_NO_CACHE_DIR: false + PIP_USER: 1 + + # Make sure poetry won't use virtual environments + POETRY_VIRTUALENVS_CREATE: false + + # Specify paths here, so we know what to cache + POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/py-user-base + PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base + +jobs: + unit-tests: + runs-on: ${{ matrix.platform }} + + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.7", "3.10"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + id: python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Add custom PYTHONUSERBASE to PATH (linux) + if: runner.os != 'Windows' + run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH + + # Windows is an absolute piece of crap, for some reason, instead of using /bin + # dir in the userbase path, it creates additional folder named differently in each version + # in which it makes Scripts/ folder (i.e. USERBASE/Python37/Scripts). To handle this + # we're using python's site module which can output it's own path, getting the dirname + # and adding /Scripts as a suffix which is then added to the path. + - name: Add custom PYTHONUSERBASE to PATH (windows) + if: runner.os == 'Windows' + run: | + dir="$(dirname $(python3 -m site --user-site))\\Scripts" + echo "$dir" >> $GITHUB_PATH + cat "$GITHUB_PATH" + shell: bash + + # Cache python dependencies so that unless we change them, + # we won't need to reinstall them with each workflow run. + # The key is a composite of multiple values, which when changed + # the cache won't be restored in order to make updating possible + - name: Python dependency caching + uses: actions/cache@v2 + id: python_cache + with: + path: ${{ env.PYTHONUSERBASE }} + key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./pyproject.toml', './poetry.lock') }}" + + # In case the dependencies weren't restored, install them + - name: Install dependencies using poetry + if: steps.python_cache.outputs.cache-hit != 'true' + run: | + pip install poetry + poetry install + + # Run the unit-tests using pytest + - name: Run pytest + run: poetry run task test diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..c83305a --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,92 @@ +name: Validation + +on: + push: + branches: + - main + pull_request: + +# Cancel already running workflows if new ones are scheduled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Make sure pip caches dependencies and installs as user + PIP_NO_CACHE_DIR: false + PIP_USER: 1 + + # Make sure poetry won't use virtual environments + POETRY_VIRTUALENVS_CREATE: false + + # Specify paths here, so we know what to cache + POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/py-user-base + PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base + PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Add custom PYTHONUSERBASE to PATH + run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH + + - name: Set up Python + id: python + uses: actions/setup-python@v3 + with: + python-version: "3.10.4" + + # Cache python dependencies so that unless we change them, + # we won't need to reinstall them with each workflow run. + # The key is a composite of multiple values, which when changed + # the cache won't be restored in order to make updating possible + - name: Python dependency caching + uses: actions/cache@v2 + id: python_cache + with: + path: ${{ env.PYTHONUSERBASE }} + key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./pyproject.toml', './poetry.lock') }}" + + # In case the dependencies weren't restored, install them + - name: Install dependencies using poetry + if: steps.python_cache.outputs.cache-hit != 'true' + run: | + pip install poetry + poetry install + + # Cache pre-commit environment + # The key is a composite of multiple values, which when changed + # the cache won't be restored in order to make updating possible + - name: Pre-commit Environment Caching + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: "precommit-0-${{ runner.os }}-${{ env.PRE_COMMIT_HOME }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./.pre-commit-config.yaml') }}" + + # Run the actual linting steps here: + + # We skip the major hooks here, because we want to see their output, and + # success status separately in another task. They are ran below. + - name: Run general pre-commit hooks + run: PIP_USER=0; SKIP=flake8,black,isort,pyright pre-commit run --all-files + + - name: Run flake8 linter + run: flake8 . + + - name: Run black formatter (check) + run: black --check . + + - name: Run isort import formatter (check) + run: isort --check . + + - name: Run pyright type checker + run: pyright -v $PYTHONUSERBASE diff --git a/.gitignore b/.gitignore index 98d5e1d..79236ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,36 @@ -# Python cache +# Python byte-compiled / optimized / DLL files __pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Pytest coverage reports +.coverage +htmlcov/ # Editor generated files .idea/ .vscode/ .spyproject/ - -# VirtualENV files -env/ -venv/ -.venv/ +.vimrc +.nvimrc +.exerc # Environmental and personal files *.env TODO -# Database files -*.sqlite3 -*.sqlite -*.sql -*.db +# Local gitignore (symlinked to .git/info/exclude) +.gitignore_local -# Ignore personal log files +# Ignore generated log files logs/ -# MacOS generatd file +# Auto-generated folder attributes for MacOS .DS_STORE diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3e1061..0db164e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,8 @@ repos: rev: v4.0.1 hooks: - id: check-merge-conflict - - id: check-toml + - id: check-toml # For pyproject.toml + - id: check-yaml # For workflows - id: end-of-file-fixer - id: trailing-whitespace args: [--markdown-linebreak-ext=md] @@ -13,14 +14,42 @@ repos: - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 hooks: - - id: python-check-blanket-noqa - - id: python-use-type-annotations + - id: python-check-blanket-noqa # Enforce specific noqa annotations (noqa: F401,W203) + - id: python-use-type-annotations # Enforce type annotations instead of type comments + + - repo: local + hooks: + - id: black + name: Black + description: Auto-format the code with black + entry: poetry run black + language: system + types: [python] + + - repo: local + hooks: + - id: isort + name: ISort + description: Sort imports with isort + entry: poetry run isort + language: system + types: [python] - repo: local hooks: - id: flake8 name: Flake8 description: Run flake8 checks on the code - entry: pipenv run flake8 + entry: poetry run flake8 + language: system + types: [python] + + - repo: local + hooks: + - id: pyright + name: Pyright + description: Run pyright type checker + entry: poetry run pyright language: system types: [python] + pass_filenames: false # pyright runs for the entire project, it can't run for single files diff --git a/LICENSE b/LICENSE index db888d7..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,190 +1,674 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2021 Sunrit Jana - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 86c66b8..0000000 --- a/Pipfile +++ /dev/null @@ -1,24 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -colorama = "*" -rsa = "*" -rich = "*" - -[dev-packages] -flake8 = "*" -autopep8 = "*" -pre-commit = "*" -ipython = "*" -flake8-annotations = "*" -flake8-bugbear = "*" -flake8-import-order = "*" - -[scripts] -server = "python -m app.server" -client = "python -m app.client" -precommit = "pre-commit install" -lint = "pre-commit run --all-files" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index c035509..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,365 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "7f04c1487a4059230ca115cf467b2deff78e4d237e3378c1526b3a3ae6d7d591" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "index": "pypi", - "version": "==0.4.4" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "pyasn1": { - "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "rich": { - "hashes": [ - "sha256:4949e73de321784ef6664ebbc854ac82b20ff60b2865097b93f3b9b41e30da27", - "sha256:bbe04dd6ac09e4b00d22cb1051aa127beaf6e16c3d8687b026e96d3fca6aad52" - ], - "index": "pypi", - "version": "==10.16.1" - }, - "rsa": { - "hashes": [ - "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", - "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" - ], - "index": "pypi", - "version": "==4.8" - } - }, - "develop": { - "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" - }, - "autopep8": { - "hashes": [ - "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979", - "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f" - ], - "index": "pypi", - "version": "==1.6.0" - }, - "backcall": { - "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" - ], - "version": "==0.2.0" - }, - "cfgv": { - "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" - }, - "decorator": { - "hashes": [ - "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374", - "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" - ], - "markers": "python_version >= '3.5'", - "version": "==5.1.0" - }, - "distlib": { - "hashes": [ - "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", - "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579" - ], - "version": "==0.3.4" - }, - "filelock": { - "hashes": [ - "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", - "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.2" - }, - "flake8": { - "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" - ], - "index": "pypi", - "version": "==4.0.1" - }, - "flake8-annotations": { - "hashes": [ - "sha256:3edfbbfb58e404868834fe6ec3eaf49c139f64f0701259f707d043185545151e", - "sha256:52e53c05b0c06cac1c2dec192ea2c36e85081238add3bd99421d56f574b9479b" - ], - "index": "pypi", - "version": "==2.7.0" - }, - "flake8-bugbear": { - "hashes": [ - "sha256:179e41ddae5de5e3c20d1f61736feeb234e70958fbb56ab3c28a67739c8e9a82", - "sha256:8b04cb2fafc6a78e1a9d873bd3988e4282f7959bb6b0d7c1ae648ec09b937a7b" - ], - "index": "pypi", - "version": "==21.11.29" - }, - "flake8-import-order": { - "hashes": [ - "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543", - "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92" - ], - "index": "pypi", - "version": "==0.18.1" - }, - "identify": { - "hashes": [ - "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c", - "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.4.1" - }, - "ipython": { - "hashes": [ - "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97", - "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c" - ], - "index": "pypi", - "version": "==7.30.1" - }, - "jedi": { - "hashes": [ - "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", - "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" - ], - "markers": "python_version >= '3.6'", - "version": "==0.18.1" - }, - "matplotlib-inline": { - "hashes": [ - "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", - "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.3" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "nodeenv": { - "hashes": [ - "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", - "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" - ], - "version": "==1.6.0" - }, - "parso": { - "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" - ], - "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pexpect": { - "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.8.0" - }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" - }, - "platformdirs": { - "hashes": [ - "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", - "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" - ], - "markers": "python_version >= '3.7'", - "version": "==2.4.1" - }, - "pre-commit": { - "hashes": [ - "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e", - "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65" - ], - "index": "pypi", - "version": "==2.16.0" - }, - "prompt-toolkit": { - "hashes": [ - "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6", - "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.24" - }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" - }, - "pyflakes": { - "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "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" - }, - "traitlets": { - "hashes": [ - "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7", - "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033" - ], - "markers": "python_version >= '3.7'", - "version": "==5.1.1" - }, - "virtualenv": { - "hashes": [ - "sha256:7f9e9c2e878d92a434e760058780b8d67a7c5ec016a66784fe4b0d5e50a4eb5c", - "sha256:efd556cec612fd826dc7ef8ce26a6e4ba2395f494244919acd135fb5ceffa809" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.11.2" - }, - "wcwidth": { - "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" - ], - "version": "==0.2.5" - } - } -} diff --git a/README.md b/README.md index be138a2..a7cf577 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,28 @@ -# ZeroCom 🚀 +

ZeroCOM 🚀

-[![Twitter: janaSunrise](https://img.shields.io/twitter/follow/janaSunrise.svg?style=social)](https://twitter.com/janaSunrise) +

Powerful chat application, built using Python. ✨

-A secure and advanced chat application in Python. + +

+ + Twitter + -## Installation + + Maintained + +

+ + +

+ Report a bug + · + Discussions +

+ +## 🚀 Installation + +**Python 3.7 or above is required!** The project uses pipenv for dependencies. Here's how to install the dependencies: @@ -14,12 +32,11 @@ pipenv sync -d ## Usage -The server uses a configuration file (`config.ini`) located in the root of the project to -run it. It is configured to run in `127.0.0.1:5700` TCP port by default. You can change things -as you need and configure according to you. +The server uses a configuration file (`config.ini`) located in the root of the project to run it. +It is configured to run in `127.0.0.1:5700` TCP port by default. You can change things as you need +and configure according to you. -To connect to the ZeroCom server, It is essential to have a ZeroCom client to establish -connection and use it. +To connect to the ZeroCom server, It is essential to have a ZeroCom client to establish connection and use it. #### Running the server @@ -73,16 +90,10 @@ And you can also use nested tags together as following, `[blue]Blueeee [bold]bol And finally, You can use emojis easily! Here's a example: `Star emoji - :star:`, and `:star:` gets converted into ⭐ -## Future plans, Bugs and Issues - -For the future plans and more, check out the project board: https://github.com/janaSunrise/ZeroCOM/projects. - -To check the bugs and issues currently in the code, check here: https://github.com/janaSunrise/ZeroCOM/issues. - ## 🤝 Contributing -Contributions, issues and feature requests are welcome. After cloning & setting up project locally, you can just submit -a PR to this repo and it will be deployed once it's accepted. +Contributions, issues and feature requests are welcome. After cloning & setting up project locally, you can +just submit a PR to this repo and it will be deployed once it's accepted. ⚠️ It’s good to have descriptive commit messages, or PR titles so that other contributors can understand about your commit or the PR Created. Read [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.3/) before @@ -93,7 +104,7 @@ making the commit message. If you have various suggestions, questions or want to discuss things with our community, Have a look at [Github discussions](https://github.com/janaSunrise/ZeroCom/discussions)! -## Show your support +## 👇 Show your support We love people's support in growing and improving. Be sure to leave a ⭐️ if you like the project and also be sure to contribute, if you're interested! diff --git a/app/__main__.py b/app/__main__.py deleted file mode 100644 index 633724f..0000000 --- a/app/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -if __name__ == "__main__": - print("This module is not designed to run on it's own.") - print("Please run the server or client whichever is needed.") diff --git a/app/client.py b/app/client.py deleted file mode 100644 index f3c9b25..0000000 --- a/app/client.py +++ /dev/null @@ -1,63 +0,0 @@ -import errno -import select -import sys -import time - -from .models.client import Client -from .utils.contextmanagers import Timer - -if __name__ == "__main__": - if len(sys.argv) != 5: - print("Usage: python -m client ") - sys.exit(1) - - _, SERVER_IP, PORT, USERNAME, PASSWORD = sys.argv - PORT = int(PORT) - - # Initialize the client object - client = Client((SERVER_IP, PORT), USERNAME) - - # Connect and initialize - client.connect() - client.initialize() - - # Print the initial message logging. - client.logger.flash("Welcome to the chat. CTRL+C to disconnect. Happy chatting!\n") - client.logger.message("ME", "", end="") - - while True: - SOCKETS = [sys.stdin, client.socket] - - try: - ready_to_read, ready_to_write, in_error = select.select(SOCKETS, [], []) - except KeyboardInterrupt: - print() - client.logger.info("Disconnecting, hold on.") - - with Timer(lambda x: client.logger.success(f"Disconnected successfully in {x}ms.")): - client.disconnect() - - sys.exit(0) - - for run_sock in ready_to_read: - if run_sock == client.socket: - try: - username, message = client.receive_message() - - print() - client.logger.message(username, message) - client.logger.message("ME", "", end="") - - sys.stdout.flush() - except IOError as e: - if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK: - client.logger.error(f"Error occured while reading: {str(e)}") - sys.exit() - - continue - else: - message = sys.stdin.readline() - client.logger.message("ME", "", end="") - sys.stdout.flush() - - client.send_message(message) diff --git a/app/config.py b/app/config.py deleted file mode 100644 index 00dc8f7..0000000 --- a/app/config.py +++ /dev/null @@ -1,24 +0,0 @@ -from textwrap import dedent - -from .utils import config_parser, get_bright_color - -# Constants. -BANNER = dedent(f"""{get_bright_color("CYAN")} - ____ _____ -/_ / ___ _______ / ___/__ __ _ - / /_/ -_) __/ _ \\/ /__/ _ \\/ ' \\ -/___/\\__/_/ \\___/\\___/\\___/_/_/_/ -""") - -# Server related config. -IP = config_parser("server", "IP") -PORT = config_parser("server", "port", cast=int) -HEADER_LENGTH = config_parser("server", "HEADER_LEN", cast=int) -MOTD = config_parser("server", "MOTD") - -# Authentication config. -PASSWORD = config_parser("auth", "PASSWORD") - -# Max connections. -MAX_CONNECTIONS = config_parser("server", "MAX_CONNECTIONS") -MAX_CONNECTIONS = None if MAX_CONNECTIONS == "" else MAX_CONNECTIONS diff --git a/app/constants.py b/app/constants.py deleted file mode 100644 index 119ea0f..0000000 --- a/app/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -# Version -VERSION = "1.0.0" - -# Config file -CONFIG_FILE = "config.ini" diff --git a/app/encryption/rsa.py b/app/encryption/rsa.py deleted file mode 100644 index dde5d10..0000000 --- a/app/encryption/rsa.py +++ /dev/null @@ -1,20 +0,0 @@ -import rsa -from rsa.key import AbstractKey, PrivateKey, PublicKey - - -class RSA: - @classmethod - def generate_keys(cls, size: int = 512) -> tuple: - return rsa.newkeys(size) - - @classmethod - def export_key_pkcs1(cls, public_key: PublicKey, format: str = "PEM") -> bytes: - return PublicKey.save_pkcs1(public_key, format=format) - - @classmethod - def load_key_pkcs1(cls, public_key_pem: bytes) -> AbstractKey: - return PublicKey.load_pkcs1(public_key_pem) - - @classmethod - def sign_message(cls, message: bytes, private_key: PrivateKey, algorithm: str = "SHA-1") -> bytes: - return rsa.sign(message, private_key, algorithm) diff --git a/app/mixins/logging.py b/app/mixins/logging.py deleted file mode 100644 index 3c9dac2..0000000 --- a/app/mixins/logging.py +++ /dev/null @@ -1,11 +0,0 @@ -from ..utils import Logger - - -class LoggingMixin: - @property - def logger(self) -> Logger: - try: - return self._log - except AttributeError: - self._log = Logger() - return self._log diff --git a/app/models/client.py b/app/models/client.py deleted file mode 100644 index 6c0ea95..0000000 --- a/app/models/client.py +++ /dev/null @@ -1,110 +0,0 @@ -import socket -import sys -import time -import typing as t - -from ..config import HEADER_LENGTH -from ..encryption.rsa import RSA -from ..mixins.logging import LoggingMixin -from ..utils import on_startup - - -class Client(LoggingMixin): - __slots__ = ( - "host", - "port", - "username", - "socket", - "start_timer", - "startup_duration", - "PRIVATE_KEY", - "PUBLIC_KEY", - "motd" - ) - - def __init__(self, address: tuple, username: str) -> None: - self.host, self.port = address - self.username = username - - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.start_timer = time.perf_counter() - self.startup_duration = None - - self.PUBLIC_KEY, self.PRIVATE_KEY = RSA.generate_keys(512) - - self.motd = None - - @staticmethod - def get_header(message: bytes) -> bytes: - return f"{len(message):<{HEADER_LENGTH}}".encode() - - def connect(self) -> None: - try: - self.socket.connect((self.host, self.port)) - except ConnectionRefusedError: - on_startup("Client") - - self.logger.error("Connection could not be established. Invalid HOST/PORT.") - sys.exit(1) - - def disconnect(self) -> None: - self.socket.close() - - def display_connected_banner(self) -> None: - end = time.perf_counter() - self.startup_duration = round((end - self.start_timer) * 1000, 2) - - on_startup("Client", self.startup_duration, motd=self.motd) - - self.logger.success(f"Connected to remote host at [{self.host}:{self.port}]") - - def initialize(self) -> None: - # Send the specified uname. - uname = self.username.encode() - uname_header = self.get_header(uname) - - # Key auth - exported_public_key = RSA.export_key_pkcs1(self.PUBLIC_KEY, "PEM") - public_key_header = self.get_header(exported_public_key) - - # Send the message - self.socket.send(uname_header + uname) - self.socket.send(public_key_header + exported_public_key) - - # Receive the MOTD - motd_len = int(self.socket.recv(HEADER_LENGTH).decode().strip()) - self.motd = self.socket.recv(motd_len).decode().strip() - - # Set blocking to false. - self.socket.setblocking(False) - - # Display banner - self.display_connected_banner() - - def receive_message(self) -> tuple: - username_header = self.socket.recv(HEADER_LENGTH) - - if not len(username_header): - self.logger.error("Server has closed the connection.") - sys.exit(1) - - username_len = int(username_header.decode().strip()) - username = self.socket.recv(username_len).decode() - - msg_length = int(self.socket.recv(HEADER_LENGTH).decode().strip()) - msg = self.socket.recv(msg_length).decode() - - return username, msg - - def send_message(self, message: t.Optional[str] = None) -> None: - if message: - message_bytes = message.replace("\n", "").encode() - message_header = self.get_header(message_bytes) - - # Key auth - key_sign = RSA.sign_message(message_bytes, self.PRIVATE_KEY) - key_sign_header = self.get_header(key_sign) - - self.socket.send(key_sign_header + key_sign) - self.socket.send(message_header + message_bytes) diff --git a/app/models/message.py b/app/models/message.py deleted file mode 100644 index 7344892..0000000 --- a/app/models/message.py +++ /dev/null @@ -1,10 +0,0 @@ -import typing as t - - -class Message: - def __init__(self, header: t.Optional[bytes], data: bytes) -> None: - self.header = header - self.data = data - - def __str__(self) -> str: - return self.data.decode() diff --git a/app/models/server.py b/app/models/server.py deleted file mode 100644 index a88f8c2..0000000 --- a/app/models/server.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -import socket -import sys -import time -import typing as t - -import rsa - -from .message import Message -from .server_side_client import Client -from ..config import HEADER_LENGTH, MOTD -from ..mixins.logging import LoggingMixin -from ..utils import get_color, on_startup - - -class Server(LoggingMixin): - __slots__ = ( - "sockets_list", - "clients", - "host", - "port", - "socket", - "start_timer", - "startup_duration", - "backlog", - "motd" - ) - - def __init__(self, address: tuple, backlog: t.Optional[int] = None) -> None: - # List of sockets and clients - self.sockets_list = [] - self.clients = {} - - # Address to run the server on - self.host, self.port = address - - # Initialize the main sockets. - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if os.name == "posix": - # REUSE_ADDR works differently on windows - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # Initialize startup timer and calculate duration - self.start_timer = time.perf_counter() - self.startup_duration = None - - # Get the backlog (Max number of connections at a time) - self.backlog = backlog - - # MOTD of the server - self.motd = MOTD - - def connect(self) -> None: - try: - self.socket.bind((self.host, self.port)) - except OSError as exc: - self.socket.close() - - on_startup("Server") - self.logger.error(f"Server could not be initialized. Error: {exc}") - - sys.exit(1) - else: - end = time.perf_counter() - duration = round((end - self.start_timer) * 1000, 2) - - on_startup("Server", duration, self.host, self.port) - - # Listening backlog - if not self.backlog: - self.socket.listen() - else: - self.socket.listen(int(self.backlog)) - - # Set socket to non-blocking - self.socket.setblocking(False) - - # Add socket to the list of sockets. - self.sockets_list.append(self.socket) - - self.logger.success("Server started. Listening for connections.") - - def disconnect(self) -> None: - for current_socket in self.sockets_list: - current_socket.close() - - def remove_specified_socket(self, sock: socket.socket) -> None: - self.sockets_list.remove(sock) - del self.clients[sock] - - def remove_errored_sockets(self, errored_sockets: list) -> None: - for current_socket in errored_sockets: - client = self.clients[current_socket] - self.logger.warning( - f"{get_color('YELLOW')}Exception occurred. Location: {client.username} [{client.address}]" - ) - - self.remove_specified_socket(current_socket) - - def receive_message(self, client_socket: socket.socket) -> t.Optional[Message]: - try: - message_header = client_socket.recv(HEADER_LENGTH) - if not len(message_header): - return - - message_length = int(message_header.decode().strip()) - - return Message(message_header, client_socket.recv(message_length)) - except Exception as exc: - self.logger.error(f"Exception occurred: {exc}") - - def process_connection(self) -> None: - client_socket, address = self.socket.accept() - - username = self.receive_message(client_socket) - pub_key = self.receive_message(client_socket) - - client = Client(client_socket, address, username, pub_key) - - if not username: - self.logger.error(f"New connection failed from {client.address}.") - return - - if not pub_key: - self.logger.error(f"New connection failed from {client.address}. No key auth found. {pub_key}") - return - - self.sockets_list.append(client_socket) - self.clients[client_socket] = client - - # Log successful connection - self.logger.success( - f"{get_color('GREEN')}Accepted new connection requested by {client.username} [{client.address}]." - ) - - # Send the data to Client - motd = self.motd.encode() - motd_header = client.get_header(motd) - - # Sent the MOTD - client.socket.send(motd_header + motd) - - def broadcast_message(self, sock: socket.socket, client: Client, message: Message) -> None: - for client_socket in self.clients: - if client_socket != sock: - sender_information = client.username_header + client.raw_username - message_to_send = message.header + message.data - - client_socket.send(sender_information + message_to_send) - - def process_message(self, client_socket: socket.socket) -> None: - # Receive Signature and message - sign = self.receive_message(client_socket) - message = self.receive_message(client_socket) - - # If disconnected - if not message or not sign: - client = self.clients[client_socket] - - self.logger.error(f"Connection closed [{client.username}@{client.address}]") - self.remove_specified_socket(client_socket) - - return - - # Get the client - client = self.clients[client_socket] - - # Verify key - try: - if rsa.verify(message.data, sign.data, client.pub_key): - msg = message.data.decode() - - self.logger.message(client.username, msg) - self.broadcast_message(client_socket, client, message) - except rsa.VerificationError: - self.logger.warning( - f"Received incorrect verification from {client.address} [{client.username}] | " - f"message:{message.data.decode()})" - ) - - warning = Message(None, "Messaging failed from user due to incorrect verification.".encode()) - warning.header = client.get_header(warning.data) - - self.broadcast_message(client_socket, client, warning) - return diff --git a/app/models/server_side_client.py b/app/models/server_side_client.py deleted file mode 100644 index 01153bd..0000000 --- a/app/models/server_side_client.py +++ /dev/null @@ -1,47 +0,0 @@ -import socket -import typing as t - -from .message import Message -from ..config import HEADER_LENGTH -from ..encryption.rsa import RSA - - -class Client: - __slots__ = ( - "socket", - "ip", - "port", - "address", - "username_header", - "raw_username", - "username", - "pub_key_header", - "pub_key_pem", - "pub_key", - ) - - def __init__( - self, - client_socket: socket.socket, - address: t.Union[list, tuple], - username: Message, - pub_key: Message - ) -> None: - self.socket = client_socket - - self.ip, self.port = address - self.address = f"{address[0]}:{address[1]}" - - self.username_header = username.header - self.raw_username = username.data - self.username = self.raw_username.decode() - - if pub_key: - self.pub_key_header = pub_key.header - self.pub_key_pem = pub_key.data - - self.pub_key = RSA.load_key_pkcs1(self.pub_key_pem) - - @staticmethod - def get_header(message: str) -> bytes: - return f"{len(message):<{HEADER_LENGTH}}".encode() diff --git a/app/server.py b/app/server.py deleted file mode 100644 index 5747770..0000000 --- a/app/server.py +++ /dev/null @@ -1,33 +0,0 @@ -import select -import sys - -from .config import IP, MAX_CONNECTIONS, PORT -from .models.server import Server -from .utils.contextmanagers import Timer - -if __name__ == "__main__": - # Initialize the socket - server = Server((IP, PORT), MAX_CONNECTIONS) - - # Connect to the server - server.connect() - - while True: - try: - ready_to_read, _, in_error = select.select(server.sockets_list, [], server.sockets_list) - except KeyboardInterrupt: - server.logger.info("Server stopping...") - - with Timer(lambda x: server.logger.success(f"Server stopped successfully in {x}ms.")): - server.disconnect() - - sys.exit(0) - - for socket_ in ready_to_read: - if socket_ == server.socket: - server.process_connection() - else: - server.process_message(socket_) - - # Remove errored connections - server.remove_errored_sockets(in_error) diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index d750382..0000000 --- a/app/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .colors import get_bright_color, get_color -from .config_loader import config_parser -from .console import clear_screen -from .logger import Logger -from .startup import on_startup diff --git a/app/utils/colors.py b/app/utils/colors.py deleted file mode 100644 index 329a10d..0000000 --- a/app/utils/colors.py +++ /dev/null @@ -1,11 +0,0 @@ -import colorama - -colorama.init(autoreset=True) - - -def get_color(color: str) -> str: - return getattr(colorama.Fore, color.upper()) - - -def get_bright_color(color: str) -> str: - return getattr(colorama.Style, "BRIGHT") + get_color(color) # noqa: B009 diff --git a/app/utils/config_loader.py b/app/utils/config_loader.py deleted file mode 100644 index d2c8aed..0000000 --- a/app/utils/config_loader.py +++ /dev/null @@ -1,36 +0,0 @@ -import typing as t -from configparser import ConfigParser - -from ..constants import CONFIG_FILE - -# Define global parser -parser = ConfigParser() - -# Load the config file. -parser.read(CONFIG_FILE) - -# Utility function for string to boolean. -TRUE_VALUES = {"y", "yes", "t", "true", "on", "1"} -FALSE_VALUES = {"n", "no", "f", "false", "off", "0"} - - -def strtobool(value: str) -> bool: - value = value.lower() - - if value in TRUE_VALUES: - return True - elif value in FALSE_VALUES: - return False - - raise ValueError(f"Invalid boolean value {value}") - - -def config_parser( - section: str, - variable: str, - cast: t.Type = str -) -> t.Any: - if cast is bool: - cast = strtobool - - return cast(parser.get(section, variable)) diff --git a/app/utils/console.py b/app/utils/console.py deleted file mode 100644 index 1180079..0000000 --- a/app/utils/console.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - - -def clear_screen() -> None: - if os.name == "nt": - os.system("cls") - else: - os.system("clear") diff --git a/app/utils/contextmanagers.py b/app/utils/contextmanagers.py deleted file mode 100644 index 42007b1..0000000 --- a/app/utils/contextmanagers.py +++ /dev/null @@ -1,23 +0,0 @@ -import time -import typing as t -from types import TracebackType - - -class Timer: - def __init__(self, display_func: t.Callable) -> None: - self.display_func = display_func - self.start_time = time.perf_counter() - - def __enter__(self) -> None: - ... - - def __exit__( - self, - exc_type: t.Optional[TracebackType], - exc_val: t.Optional[BaseException], - exc_tb: t.Optional[TracebackType] - ) -> None: - end_time = time.perf_counter() - duration = round((end_time - self.start_time) * 1000, 2) - - self.display_func(duration) diff --git a/app/utils/logger.py b/app/utils/logger.py deleted file mode 100644 index 26b5e28..0000000 --- a/app/utils/logger.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime - -from colorama import Back -from rich.console import Console - -from .colors import get_bright_color, get_color - - -def get_log_color_mapping(color_key: str, symbol: str) -> str: - color_map = log_color_mapping[color_key] - return f"[{color_map}{symbol}{get_color('RESET')}]" - - -# Color and log type mapping -log_color_mapping = { - "error": get_bright_color("RED"), - "warning": get_bright_color("YELLOW"), - "message": get_color("CYAN"), - "success": get_bright_color("GREEN"), - "info": get_bright_color("MAGENTA"), - "critical": get_bright_color("RED") + Back.YELLOW, - "flash": get_bright_color("BLUE"), -} - -log_mapping = { - "error": get_log_color_mapping("error", "%"), - "warning": get_log_color_mapping("warning", "!"), - "message": get_log_color_mapping("message", ">"), - "success": get_log_color_mapping("success", "+"), - "info": get_log_color_mapping("info", "#"), - "critical": get_log_color_mapping("critical", "X"), - "flash": get_log_color_mapping("flash", "-"), -} - - -class Logger: - def __init__(self): - self._console = Console() - - @staticmethod - def _append_date(message: str) -> str: - timestamp = datetime.now() - timestamp = ( - f"{get_bright_color('CYAN')}" - f"{timestamp.hour}:{timestamp.minute}:{timestamp.second}" - f"{get_bright_color('RESET')}" - ) - - return f"[{timestamp}]{message}" - - def _print_log(self, log_type: str, message: str, date: bool = True) -> None: - message_prefix = log_mapping[log_type] - message = f"{message_prefix} {log_color_mapping[log_type]}{message}" - - if date: - message = self._append_date(message) - - print(message) - - def error(self, message: str, date: bool = True) -> None: - self._print_log("error", message, date) - - def warning(self, message: str, date: bool = True) -> None: - self._print_log("warning", message, date) - - def success(self, message: str, date: bool = True) -> None: - self._print_log("success", message, date) - - def info(self, message: str, date: bool = True) -> None: - self._print_log("info", message, date) - - def critical(self, message: str, date: bool = True) -> None: - self._print_log("critical", message, date) - - def flash(self, message: str, date: bool = True) -> None: - self._print_log("flash", message, date) - - def message(self, username: str, user_message: str, date: bool = True, **kwargs) -> None: - message_prefix = log_mapping["message"] - message = f"{get_bright_color('YELLOW')} {username}{get_color('RESET')} {message_prefix} " - - if date: - message = self._append_date(message) - - print(message, end="") - self._console.print(user_message, **kwargs) diff --git a/app/utils/startup.py b/app/utils/startup.py deleted file mode 100644 index ecda8f0..0000000 --- a/app/utils/startup.py +++ /dev/null @@ -1,44 +0,0 @@ -import typing as t -from textwrap import dedent - -from .colors import get_bright_color -from .console import clear_screen -from ..constants import VERSION - - -def on_startup( - name: str, - boot_duration: t.Optional[float] = None, - ip: t.Optional[str] = None, - port: t.Optional[str] = None, - motd: t.Optional[str] = None -) -> None: - # Imports To prevent circular imports. - from ..config import BANNER - - # Variables - spaces_4 = " " - - # Generate initial message and add sections - message = dedent(f"""{BANNER} - {get_bright_color("GREEN")}ZeroCOM {name} Running. | {get_bright_color("YELLOW")}v{VERSION} - """) - - if ip: - msg = f"{spaces_4}{get_bright_color('CYAN')}Running on [IP] {ip}" - if port: - msg += f" | [PORT] {port}\n" - else: - msg += "\n" - - message += msg - - if boot_duration: - message += f"{spaces_4}{get_bright_color('YELLOW')}TOOK {boot_duration}ms to start.\n" - - if motd: - message += f"{spaces_4}{get_bright_color('CYAN')}MOTD: {motd}\n" - - # Clear and print screen - clear_screen() - print(message) diff --git a/config.ini b/config.ini deleted file mode 100644 index c493654..0000000 --- a/config.ini +++ /dev/null @@ -1,12 +0,0 @@ -[server] -IP=127.0.0.1 -PORT=5700 -HEADER_LEN=4096 - -MOTD=Welcome to Zerocom Chat! - -; Leave empty for system defined amount. Only integer allowed. -MAX_CONNECTIONS= - -[auth] -PASSWORD=12345678 diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..5539e9e --- /dev/null +++ b/config.toml @@ -0,0 +1,11 @@ +[server.config] +ip = "127.0.0.1" +port = 5000 + +motd = "Welcome to Zerocom Chat!" + +# Leave empty for system defined amount. Only integer allowed. +max-connections = 0 + +[server.auth] +password = 12345678 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3807978 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1008 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "autopep8" +version = "1.6.0" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = ">=2.8.0" +toml = "*" + +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "coverage" +version = "6.4.1" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.7.1" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "flake8-annotations" +version = "2.9.0" +description = "Flake8 Type Annotation Checks" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +attrs = ">=21.4,<22.0" +flake8 = ">=3.7" +typed-ast = {version = ">=1.4,<2.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "flake8-bugbear" +version = "22.4.25" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] + +[[package]] +name = "flake8-future-annotations" +version = "0.0.5" +description = "Verifies python 3.7+ files use from __future__ import annotations" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-tidy-imports" +version = "4.8.0" +description = "A flake8 plugin that helps you write tidier imports." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.8.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +pyreadline = {version = "*", markers = "sys_platform == \"win32\" and python_version < \"3.8\""} +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "identify" +version = "2.5.1" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mslex" +version = "0.3.0" +description = "shlex for windows" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pep8-naming" +version = "0.13.0" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.9.1" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.19.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "psutil" +version = "5.9.1" +description = "Cross-platform lib for process and system monitoring in Python." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.12.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pyreadline" +version = "2.1" +description = "A python implmementation of GNU readline." +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "pyreadline3" +version = "3.4.1" +description = "A python implementation of GNU readline." +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "pyright" +version = "1.1.252" +description = "Command line wrapper for pyright" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = {version = ">=3.7", markers = "python_version < \"3.8\""} + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "rich" +version = "12.4.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "taskipy" +version = "1.10.2" +description = "tasks runner for python projects" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +colorama = ">=0.4.4,<0.5.0" +mslex = {version = ">=0.3.0,<0.4.0", markers = "sys_platform == \"win32\""} +psutil = ">=5.7.2,<6.0.0" +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "virtualenv" +version = "20.14.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + +[[package]] +name = "zipp" +version = "3.8.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.7,<4" +content-hash = "0ef4e3970209af881d3069458dcf8a134aca29daa7ebada08b6db3983d9d0a77" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +autopep8 = [ + {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, + {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, +] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coloredlogs = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +coverage = [ + {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, + {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, + {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, + {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, + {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, + {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, + {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, + {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, + {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, + {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, + {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, + {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, + {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, + {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, + {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, + {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, + {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, +] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] +filelock = [ + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +flake8-annotations = [ + {file = "flake8-annotations-2.9.0.tar.gz", hash = "sha256:63fb3f538970b6a8dfd84125cf5af16f7b22e52d5032acb3b7eb23645ecbda9b"}, + {file = "flake8_annotations-2.9.0-py3-none-any.whl", hash = "sha256:84f46de2964cb18fccea968d9eafce7cf857e34d913d515120795b9af6498d56"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-22.4.25.tar.gz", hash = "sha256:f7c080563fca75ee6b205d06b181ecba22b802babb96b0b084cc7743d6908a55"}, + {file = "flake8_bugbear-22.4.25-py3-none-any.whl", hash = "sha256:ec374101cddf65bd7a96d393847d74e58d3b98669dbf9768344c39b6290e8bd6"}, +] +flake8-future-annotations = [ + {file = "flake8-future-annotations-0.0.5.tar.gz", hash = "sha256:da84011070c0d0f623a9c12bd738bea58bc65a3bf5eec20637020069310f8d84"}, + {file = "flake8_future_annotations-0.0.5-py3-none-any.whl", hash = "sha256:28fc9ae9e5ece5c211b810d856d984f33c0290933f24ae570731a0751c4917c6"}, +] +flake8-tidy-imports = [ + {file = "flake8-tidy-imports-4.8.0.tar.gz", hash = "sha256:df44f9c841b5dfb3a7a1f0da8546b319d772c2a816a1afefcce43e167a593d83"}, + {file = "flake8_tidy_imports-4.8.0-py3-none-any.whl", hash = "sha256:25bd9799358edefa0e010ce2c587b093c3aba942e96aeaa99b6d0500ae1bf09c"}, +] +humanfriendly = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] +identify = [ + {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, + {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mslex = [ + {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, + {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pep8-naming = [ + {file = "pep8-naming-0.13.0.tar.gz", hash = "sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5"}, + {file = "pep8_naming-0.13.0-py3-none-any.whl", hash = "sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [ + {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, + {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, +] +psutil = [ + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, + {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, + {file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"}, + {file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"}, + {file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"}, + {file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"}, + {file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"}, + {file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"}, + {file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"}, + {file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"}, + {file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"}, + {file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"}, + {file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"}, + {file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"}, + {file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"}, + {file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"}, + {file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"}, + {file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"}, + {file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"}, + {file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"}, + {file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"}, + {file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"}, + {file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"}, + {file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"}, + {file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"}, + {file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pygments = [ + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pyreadline = [ + {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, + {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, + {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, +] +pyreadline3 = [ + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, +] +pyright = [ + {file = "pyright-1.1.252-py3-none-any.whl", hash = "sha256:0f436ff34b32a9d67f81ffbe5087812ec84039bdd7ec82b471e54a9291b7eef5"}, + {file = "pyright-1.1.252.tar.gz", hash = "sha256:df48c20fca2442f2f3016aa94943bc58221e14f071a23befd5248deff25726b5"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +rich = [ + {file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"}, + {file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"}, +] +rsa = [ + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +taskipy = [ + {file = "taskipy-1.10.2-py3-none-any.whl", hash = "sha256:58d5382d90d5dd94ca8c612855377e5a98b9cb669c208ebb55d6a45946de3f9b"}, + {file = "taskipy-1.10.2.tar.gz", hash = "sha256:eae4feb74909da3ad0ca0275802e1c2f56048612529bd763feb922d284d8a253"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] +virtualenv = [ + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, +] +zipp = [ + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a4a286d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[tool.poetry] +name = "ZeroCOM" +version = "0.1.0" +description = "Powerful chat application, built using Python." +authors = ["Sunrit Jana ", "ItsDrike "] +license = "GPL-3.0-or-later" + +[tool.poetry.dependencies] +python = ">=3.7,<4" +colorama = "^0.4.4" +rsa = "^4.8" +rich = "^12.4.4" +toml = "^0.10.2" + +[tool.poetry.dev-dependencies] +flake8 = "^4.0.1" +flake8-annotations = "^2.9.0" +flake8-bugbear = "^22.4.25" +flake8-tidy-imports = "^4.8.0" +flake8-future-annotations = "^0.0.5" +pep8-naming = "^0.13.0" +autopep8 = "^1.6.0" +black = "^22.3.0" +pre-commit = "^2.19.0" +taskipy = "^1.10.2" +isort = "^5.10.1" +pyright = "^1.1.252" +pytest = "^7.1.2" +pytest-cov = "^3.0.0" + +[tool.black] +line-length = 120 +extend-exclude = "^/.cache" + +[tool.isort] +profile = "black" +line_length = 120 +order_by_type = false +case_sensitive = true +skip = [".venv", ".git", ".cache"] + +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = ["tests"] +addopts = "--strict-markers --cov=zerocom --cov-branch --cov-report=term-missing --cov-report html --no-cov-on-fail" + + +[tool.taskipy.tasks] +precommit = "pre-commit install" +lint = "pre-commit run --all-files" +format = "black . && isort ." +test = "pytest -v --failed-first" +retest = "pytest -v --last-failed" +test-nocov = "pytest -v --no-cov --failed-first" +server = "python -m zerocom.server" +client = "python -m zerocom.client" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/app/__init__.py b/tests/__init__.py similarity index 100% rename from app/__init__.py rename to tests/__init__.py diff --git a/app/encryption/__init__.py b/tests/protocol/__init__.py similarity index 100% rename from app/encryption/__init__.py rename to tests/protocol/__init__.py diff --git a/tests/protocol/helpers.py b/tests/protocol/helpers.py new file mode 100644 index 0000000..4a69c48 --- /dev/null +++ b/tests/protocol/helpers.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from typing import Optional +from unittest.mock import Mock + +from zerocom.protocol.abc import BaseReader, BaseWriter + + +class Reader(BaseReader): + """Testable concrete implementation of BaseReader ABC.""" + + def read(self, length: int) -> bytearray: + """Concrete implementation of abstract read method. + + Since classes using abc.ABC can't be initialized if they have any abstract methods + which weren't overridden with a concrete implementation, this is a fake implementation, + without any actual logic, purely to allow the initialization of this class. + + This method is expected to be mocked using ReadFunctionMock if it's expected to get called + during testing. If this method gets called without being mocked, it will raise NotImplementedError. + """ + raise NotImplementedError( + "This concrete override of abstract read method isn't intended for actual use!\n" + " - If you're writing a new test, did you forget to mock it?\n" + " - If you're seeing this in an existing test, this method got called without the test expecting it," + " this probably means you changed something in the code leading to this call, but you haven't updated" + " the tests to mock this function." + ) + + +class Writer(BaseWriter): + """Initializable concrete implementation of BaseWriter ABC.""" + + def write(self, data: bytearray) -> None: + """Concrete implementation of abstract write method. + + Since classes using abc.ABC can't be initialized if they have any abstract methods + which weren't overridden with a concrete implementation, this is a fake implementation, + without any actual logic, purely to allow the initialization of this class. + + This method is expected to be mocked using WriteFunctionMock if it's expected to get called + during testing. If this method gets called without being mocked, it will raise NotImplementedError. + """ + raise NotImplementedError( + "This concrete override of abstract write method isn't intended for actual use!\n" + " - If you're writing a new test, did you forget to mock it?\n" + " - If you're seeing this in an existing test, this method got called without the test expecting it," + " this probably means you changed something in the code leading to this call, but you haven't updated" + " the tests to mock this function." + ) + + +class ReadFunctionMock(Mock): + def __init__(self, *a, combined_data: Optional[bytearray] = None, **kw): + super().__init__(*a, **kw) + if combined_data is None: + combined_data = bytearray() + self.combined_data = combined_data + + def __call__(self, length: int) -> bytearray: + """Override mock's __call__ to make it return part of our combined_data bytearray. + + This allows us to define the combined data we want the mocked read function to be + returning, and have each call only take requested part (length) of that data. + """ + self.return_value = self.combined_data[:length] + del self.combined_data[:length] + return super().__call__(length) + + def assert_read_everything(self) -> None: + """Ensure that the passed combined_data was fully read and depleted by one, or more calls.""" + if len(self.combined_data) != 0: + raise AssertionError( + f"Read function didn't deplete all of it's data, remaining data: {self.combined_data!r}" + ) + + +class WriteFunctionMock(Mock): + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + self.combined_data = bytearray() + + def __call__(self, data: bytearray) -> None: + """Override mock's __call__ to extend our combined_data bytearray. + + This allows us to keep track of exactly what data was written by the mocked write function + in total, rather than only having tools like assert_called_with, which don't combine the + data of each call. + """ + self.combined_data.extend(data) + return super().__call__(data) + + def assert_has_data(self, data: bytearray, ensure_called: bool = True) -> None: + """Ensure that the total data to write by the mocked function matches expected data.""" + if ensure_called: + self.assert_called() + + if self.combined_data != data: + raise AssertionError(f"Write function mock expected data {data!r}, but was {self.call_data!r}") diff --git a/tests/protocol/test_abc.py b/tests/protocol/test_abc.py new file mode 100644 index 0000000..ac6a060 --- /dev/null +++ b/tests/protocol/test_abc.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import pytest + +from tests.protocol.helpers import ReadFunctionMock, Reader, WriteFunctionMock, Writer + + +class TestReader: + @classmethod + def setup_class(cls): + """Initialize writer instance to be tested.""" + cls.reader = Reader() + + @pytest.fixture + def read_mock(self, monkeypatch: pytest.MonkeyPatch): + """Monkeypatch the read function with a mock which is returned.""" + mock_f = ReadFunctionMock() + monkeypatch.setattr(self.reader.__class__, "read", mock_f) + yield mock_f + + # Run this assertion after the test, to ensure that all specified data + # to be read, actually was read + mock_f.assert_read_everything() + + @pytest.mark.parametrize( + "read_bytes,expected_value", + ( + ([10], 10), + ([255], 255), + ([0], 0), + ), + ) + def test_read_ubyte(self, read_bytes: list[int], expected_value: int, read_mock: ReadFunctionMock): + """Reading byte int should return an integer in a single unsigned byte.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_ubyte() == expected_value + + @pytest.mark.parametrize( + "read_bytes,expected_value", + ( + ([236], -20), + ([128], -128), + ([20], 20), + ([127], 127), + ), + ) + def test_read_byte(self, read_bytes: list[int], expected_value: int, read_mock: ReadFunctionMock): + """Negative number bytes should be read from two's complement format.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_byte() == expected_value + + @pytest.mark.parametrize( + "read_bytes,expected_value", + ( + ([0], 0), + ([1], 1), + ([2], 2), + ([15], 15), + ([127], 127), + ([128, 1], 128), + ([129, 1], 129), + ([255, 1], 255), + ([192, 132, 61], 1000000), + ([255, 255, 255, 255, 7], 2147483647), + ), + ) + def test_read_varint(self, read_bytes: list[int], expected_value: int, read_mock: ReadFunctionMock): + """Reading varint bytes results in correct values.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_varint() == expected_value + + @pytest.mark.parametrize( + "read_bytes,expected_value", + ( + ([0], 0), + ([154, 1], 154), + ([255, 255, 3], 2**16 - 1), + ), + ) + def test_read_varint_max_size(self, read_bytes: list[int], expected_value: int, read_mock: ReadFunctionMock): + """Varint reading should be limitable to n max bytes and work with values in range.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_varint(max_size=2) == expected_value + + def test_read_varnum_max_size_out_of_range(self, read_mock: ReadFunctionMock): + """Varint reading limited to n max bytes should raise an IOError for numbers out of this range.""" + read_mock.combined_data = bytearray([128, 128, 4]) + with pytest.raises(IOError): + self.reader.read_varint(max_size=2) + + @pytest.mark.parametrize( + "read_bytes,expected_string", + ( + ([len("test")] + list(map(ord, "test")), "test"), + ([len("a" * 100)] + list(map(ord, "a" * 100)), "a" * 100), + ([0], ""), + ), + ) + def test_read_utf(self, read_bytes: list[int], expected_string: str, read_mock: ReadFunctionMock): + """Reading UTF string results in correct values.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_utf() == expected_string + + @pytest.mark.parametrize( + "read_bytes,expected_bytes", + ( + ([1, 1], [1]), + ([0], []), + ([5, 104, 101, 108, 108, 111], [104, 101, 108, 108, 111]), + ), + ) + def test_read_bytearray(self, read_bytes: list[int], expected_bytes: list[int], read_mock: ReadFunctionMock): + """Writing a bytearray results in correct bytes.""" + read_mock.combined_data = bytearray(read_bytes) + assert self.reader.read_bytearray() == bytearray(expected_bytes) + + +class TestWriter: + @classmethod + def setup_class(cls): + """Initialize writer instance to be tested.""" + cls.writer = Writer() + + @pytest.fixture + def write_mock(self, monkeypatch: pytest.MonkeyPatch): + """Monkeypatch the write function with a mock which is returned.""" + mock_f = WriteFunctionMock() + monkeypatch.setattr(self.writer.__class__, "write", mock_f) + return mock_f + + def test_write_byte(self, write_mock: WriteFunctionMock): + """Writing byte int should store an integer in a single byte.""" + self.writer.write_byte(15) + write_mock.assert_has_data(bytearray([15])) + + def test_write_byte_negative(self, write_mock: WriteFunctionMock): + """Negative number bytes should be stored in two's complement format.""" + self.writer.write_byte(-20) + write_mock.assert_has_data(bytearray([236])) + + def test_write_byte_out_of_range(self): + """Signed bytes should only allow writes from -128 to 127.""" + with pytest.raises(ValueError): + self.writer.write_byte(-129) + with pytest.raises(ValueError): + self.writer.write_byte(128) + + def test_write_ubyte(self, write_mock: WriteFunctionMock): + """Writing unsigned byte int should store an integer in a single byte.""" + self.writer.write_byte(80) + write_mock.assert_has_data(bytearray([80])) + + def test_write_ubyte_out_of_range(self): + """Unsigned bytes should only allow writes from 0 to 255.""" + with pytest.raises(ValueError): + self.writer.write_ubyte(256) + with pytest.raises(ValueError): + self.writer.write_ubyte(-1) + + @pytest.mark.parametrize( + "number,expected_bytes", + ( + (0, [0]), + (1, [1]), + (2, [2]), + (15, [15]), + (127, [127]), + (128, [128, 1]), + (129, [129, 1]), + (255, [255, 1]), + (1000000, [192, 132, 61]), + (2147483647, [255, 255, 255, 255, 7]), + ), + ) + def test_write_varint(self, number: int, expected_bytes: list[int], write_mock: WriteFunctionMock): + """Writing varints results in correct bytes.""" + self.writer.write_varint(number) + write_mock.assert_has_data(bytearray(expected_bytes)) + + def test_write_varint_out_of_range(self): + """Varint without max size should only work with positive integers.""" + with pytest.raises(ValueError): + self.writer.write_varint(-1) + + @pytest.mark.parametrize( + "number,expected_bytes", + ( + (0, [0]), + (154, [154, 1]), + (2**16 - 1, [255, 255, 3]), + ), + ) + def test_write_varint_max_size(self, number: int, expected_bytes: list[int], write_mock: WriteFunctionMock): + """Varints should be limitable to n max bytes and work with values in range.""" + self.writer.write_varint(number, max_size=2) + write_mock.assert_has_data(bytearray(expected_bytes)) + + def test_write_varint_max_size_out_of_range(self): + """Varints limited to n max bytes should raise ValueErrors for numbers out of this range.""" + with pytest.raises(ValueError): + self.writer.write_varint(2**16, max_size=2) + + @pytest.mark.parametrize( + "string,expected_bytes", + ( + ("test", [len("test")] + list(map(ord, "test"))), + ("a" * 100, [len("a" * 100)] + list(map(ord, "a" * 100))), + ("", [0]), + ), + ) + def test_write_utf(self, string: str, expected_bytes: list[int], write_mock: WriteFunctionMock): + """Writing UTF string results in correct bytes.""" + self.writer.write_utf(string) + write_mock.assert_has_data(bytearray(expected_bytes)) + + @pytest.mark.parametrize( + "input_bytes,expected_bytes", + ( + ([1], [1, 1]), + ([], [0]), + ([104, 101, 108, 108, 111], [5, 104, 101, 108, 108, 111]), + ), + ) + def test_write_bytearray(self, input_bytes: list[int], expected_bytes: list[int], write_mock: WriteFunctionMock): + """Writing a bytearray results in correct bytes.""" + self.writer.write_bytearray(bytearray(input_bytes)) + write_mock.assert_has_data(bytearray(expected_bytes)) diff --git a/tests/protocol/test_buffer.py b/tests/protocol/test_buffer.py new file mode 100644 index 0000000..11152e0 --- /dev/null +++ b/tests/protocol/test_buffer.py @@ -0,0 +1,76 @@ +import pytest + +from zerocom.protocol.buffer import Buffer + + +def test_write(): + """Writing into the buffer should store data.""" + buf = Buffer() + buf.write(b"Hello") + assert buf, bytearray(b"Hello") + + +def test_read(): + """Reading from buffer should return stored data.""" + buf = Buffer(b"Reading is cool") + data = buf.read(len(buf)) + assert data == b"Reading is cool" + + +def test_read_multiple(): + """Multiple reads should deplete the data.""" + buf = Buffer(b"Something random") + data = buf.read(9) + assert data == b"Something" + data = buf.read(7) + assert data == b" random" + + +def test_no_data_read(): + """Reading more data than available should raise IOError.""" + buf = Buffer(b"Blip") + with pytest.raises(IOError): + buf.read(len(buf) + 1) + + +def test_reset(): + """Resetting should treat already read data as new unread data.""" + buf = Buffer(b"Will it reset?") + data = buf.read(len(buf)) + buf.reset() + data2 = buf.read(len(buf)) + assert data == data2 + assert data == b"Will it reset?" + + +def test_clear(): + """Clearing should remove all stored data from buffer.""" + buf = Buffer(b"Will it clear?") + buf.clear() + assert buf == bytearray() + + +def test_clear_resets_position(): + """Clearing should reset reading position for new data to be read.""" + buf = Buffer(b"abcdef") + buf.read(3) + buf.clear() + buf.write(b"012345") + data = buf.read(3) + assert data == b"012" + + +def test_clear_read_only(): + """Clearing should allow just removing the already read data.""" + buf = Buffer(b"0123456789") + buf.read(5) + buf.clear(only_already_read=True) + assert buf == bytearray(b"56789") + + +def test_flush(): + """Flushing should read all available data and clear out the buffer.""" + buf = Buffer(b"Foobar") + data = buf.flush() + assert data == b"Foobar" + assert buf == bytearray() diff --git a/tests/protocol/test_connection.py b/tests/protocol/test_connection.py new file mode 100644 index 0000000..28f4bd8 --- /dev/null +++ b/tests/protocol/test_connection.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import socket +from typing import Optional +from unittest.mock import MagicMock + +import pytest + +from tests.protocol.helpers import ReadFunctionMock, WriteFunctionMock +from zerocom.protocol.connection import SocketConnection + + +class MockSocket(MagicMock): + spec_set = socket.socket + + def __init__(self, *args, read_data: Optional[bytearray] = None, **kw) -> None: + super().__init__(*args, **kw) + self.recv = ReadFunctionMock(combined_data=read_data) + self.send = WriteFunctionMock() + + +def test_read(): + data = bytearray("hello", "utf-8") + conn = SocketConnection(MockSocket(read_data=data.copy())) + + out = conn.read(5) + + conn.socket.recv.assert_read_everything() + assert out == data + + +def test_read_more_data_than_sent(): + conn = SocketConnection(MockSocket(read_data=bytearray("test", "utf-8"))) + with pytest.raises(IOError): + conn.read(10) + + +def test_write(): + data = bytearray("hello", "utf-8") + conn = SocketConnection(MockSocket()) + + conn.write(data) + + conn.socket.send.assert_has_data(data) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 788fff6..0000000 --- a/tox.ini +++ /dev/null @@ -1,21 +0,0 @@ -[flake8] -max_line_length=120 -import-order-style=pycharm -application-import-names=app -exclude= - .venv/**, - .git/** -ignore= - # Ignore missing return type annotations for special methods - ANN204, ANN206, ANN202 - # Ignore missing type annotations - ANN101 # Init - ANN102 # cls - ANN002, # *Args - ANN003, # **Kwargs - # Allow lambdas - E731, - # Allow markdown inline HTML - MD033, - # allow __init__ imports - F401 diff --git a/zerocom/__init__.py b/zerocom/__init__.py new file mode 100644 index 0000000..2ffa2e1 --- /dev/null +++ b/zerocom/__init__.py @@ -0,0 +1,3 @@ +from zerocom.utils.log import setup_logging + +setup_logging() diff --git a/zerocom/client.py b/zerocom/client.py new file mode 100644 index 0000000..6c26f6d --- /dev/null +++ b/zerocom/client.py @@ -0,0 +1,57 @@ +import errno +import logging +import select +import sys +from typing import NoReturn + +from zerocom.network.client import Client + +log = logging.getLogger(__name__) + + +def run_client(host: str, port: int, username: str) -> NoReturn: + client = Client(username, (host, port)) + + print("Welcome to the chat. CTRL+C to disconnect. Happy chatting.") + print("\nME: ", end="") + + while True: + io_descriptors = [sys.stdin, client.socket] + try: + ready_read, ready_write, ready_error = select.select(io_descriptors, [], []) + except KeyboardInterrupt: + log.info("Connection ended") + client.socket.close() + sys.exit(0) + + for notified_descriptor in ready_read: + if notified_descriptor == client.socket: + try: + msg = client.receive() + except IOError as e: + if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK: + log.critical(f"Error occurred while reading: {e!r}") + sys.exit(1) + continue + else: + log.info(f"Received message: {msg}") + print("ME: ", end="") + sys.stdout.flush() + else: + message = sys.stdin.readline() + client.send(message) + + print("ME: ", end="") + sys.stdout.flush() + + +if __name__ == "__main__": + if len(sys.argv) != 5: + log.critical("Usage: python -m client ") + sys.exit(1) + + _, SERVER_IP, PORT, USERNAME, PASSWORD = sys.argv + PORT = int(PORT) + + # NOTE: Password is currently unused, because it's not yet implemented + run_client(SERVER_IP, PORT, USERNAME) diff --git a/zerocom/config.py b/zerocom/config.py new file mode 100644 index 0000000..53e32c8 --- /dev/null +++ b/zerocom/config.py @@ -0,0 +1,36 @@ +import os +from textwrap import dedent + +import toml + +# Hard-coded constants +VERSION = "0.1.0" +BANNER = dedent( + """ + ____ _____ +/_ / ___ _______ / ___/__ __ _ + / /_/ -_) __/ _ \\/ /__/ _ \\/ ' \\ +/___/\\__/_/ \\___/\\___/\\___/_/_/_/ +""" +) + +# Logging setting +DEBUG = bool(os.environ.get("ZEROCOM_DEBUG", 0)) +LOG_FILE = os.environ.get("ZEROCOM_LOG_FILE", None) +LOG_FILE_MAX_SIZE = int(os.environ.get("ZEROCOM_LOG_FILE_SIZE_MAX", 1_048_576)) # in bytes (default: 1MiB) + +# Config file location, in this case it's `config.toml` in root +CONFIG_FILE = os.environ.get("ZEROCOM_CONFIG_FILE", "config.toml") +config = toml.load(CONFIG_FILE) +server_config = config["server"]["config"] + + +class Config: + IP = server_config["ip"] + PORT = server_config["port"] + MOTD = server_config["motd"] + + PASSWORD = config["server"]["auth"]["password"] + + # Load the max connections, It's `None` if 0 is specified. + MAX_CONNECTIONS = server_config["max-connections"] if server_config["max-connections"] != 0 else None diff --git a/app/mixins/__init__.py b/zerocom/network/__init__.py similarity index 100% rename from app/mixins/__init__.py rename to zerocom/network/__init__.py diff --git a/zerocom/network/client.py b/zerocom/network/client.py new file mode 100644 index 0000000..7da345e --- /dev/null +++ b/zerocom/network/client.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import logging +import socket +from queue import Queue + +import rsa +from rsa.key import PublicKey + +from zerocom.protocol.connection import SocketConnection + +log = logging.getLogger(__name__) + + +class Client: + def __init__(self, username: str, server_address: tuple[str, int], timeout: float = 3): + self.username = username + self.server_host, self.server_port = server_address + self.timeout = timeout + + # Queue for incoming messages + self.queue = Queue() + + # Generate the keys needed for RSA encryption + self.public_key, self.private_key = rsa.newkeys(2048) + + # Connect and transmit username + socket = self._make_socket(server_address, timeout) + self.connection = SocketConnection(socket) + + self.connection.write_utf(self.username) + self.connection.write_utf(PublicKey.save_pkcs1(self.public_key, "PEM").decode()) + + @property + def socket(self) -> socket.socket: + return self.connection.socket + + @staticmethod + def _make_socket(server_address: tuple[str, int], timeout: float = 3) -> socket.socket: + sock = socket.create_connection(server_address, timeout=timeout) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return sock + + def send(self, message: str) -> None: + message = message.replace("\n", "") # TODO: Consider moving to removesuffix (3.9+) + key_sign = rsa.sign(message.encode(), self.private_key, "SHA-1") + + self.connection.write_utf(key_sign.decode()) + + log.info(f"Sending message {message} to server.") + self.connection.write_utf(message) + + def receive(self) -> tuple[str, str]: + username = self.connection.read_utf() + msg = self.connection.read_utf() + + return username, msg diff --git a/zerocom/network/server.py b/zerocom/network/server.py new file mode 100644 index 0000000..4ab8343 --- /dev/null +++ b/zerocom/network/server.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import logging +import os +import socket +from dataclasses import dataclass +from typing import Optional, cast + +import rsa +from rsa.key import PublicKey + +from zerocom.protocol.connection import SocketConnection + +log = logging.getLogger(__name__) + + +@dataclass +class ProcessedClient: + __slots__ = ("conn", "address", "username", "public_key") + + conn: SocketConnection + address: tuple[str, int] + + username: str + public_key: PublicKey + + @property + def socket(self) -> socket.socket: + return self.conn.socket + + +class Server: + def __init__(self, address: tuple[str, int], backlog: Optional[int] = None): + socket = self._make_socket(address, backlog) + self.connection = SocketConnection(socket) + + self.host, self.port = address + self.connected_clients: dict[socket.socket, ProcessedClient] = {} + + @property + def socket(self) -> socket.socket: + return self.connection.socket + + @property + def socket_list(self) -> list[socket.socket]: + """Produce a list of all currently used sockets.""" + sockets = [client.socket for client in self.connected_clients.values()] + sockets.append(self.socket) + return sockets + + @staticmethod + def _make_socket(address: tuple[str, int], backlog: Optional[int] = None) -> socket.socket: + """Make server socket capable of accepting new connections.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if os.name == "posix": + # Allow address reuse (fixes errors when using same address after program restarts) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + sock.setblocking(False) + + try: + sock.bind(address) + except OSError as exc: + sock.close() + log.critical(f"Unable to bind server to {address} (maybe this address is already in use?)") + raise exc + else: + log.info(f"Server bound to {address}") + + # Enable server to accept connections + if backlog is not None: + sock.listen(backlog) + else: + sock.listen() + + log.info("Listening for connections...") + + return sock + + def process_connection(self) -> None: + client_socket, address = self.socket.accept() + conn = SocketConnection(client_socket) + log.debug(f"Accepted new connection from {address}") + try: + username = conn.read_utf() + public_key = conn.read_utf() + except IOError as exc: + log.debug(f"Processing new connection from {address} failed when reading username: {exc!r}") + log.error(f"Dropping connection from {address} - username wasn't send properly when connecting.") + client_socket.close() + return + + client = ProcessedClient(conn, address, username, cast(PublicKey, PublicKey.load_pkcs1(public_key.encode()))) + self.connected_clients[client_socket] = client + + def process_message(self, client_socket: socket.socket) -> None: + client = self.connected_clients[client_socket] + + try: + key_sign = client.conn.read_utf() + msg = client.conn.read_utf() + except IOError as exc: + log.debug(f"Processing message from {client} failed: {exc}") + log.error(f"Dropping connection from {client} - sent invalid message") + client.socket.close() + del self.connected_clients[client_socket] + return + + log.info(f"Accepted message from {client}: {msg}") + + # RSA verification + broadcasting. + try: + if rsa.verify(msg.encode(), key_sign.encode(), client.public_key): + log.info(f"Message from {client} verified") + self.broadcast(client.socket, msg) + except rsa.VerificationError: + log.error(f"Dropping connection from {client} - received incorrect verification") + + # Broadcast a warning to all clients. + self.broadcast(client_socket, f"{client.username} has been kicked for incorrect verification.") + + # Close the connection. + client.socket.close() + del self.connected_clients[client_socket] + + def broadcast(self, client_socket: socket.socket, message: str) -> None: + for client_sock in self.connected_clients.values(): + if client_sock.socket != client_socket: + client_sock.conn.write_utf(client_sock.username) + client_sock.conn.write_utf(message) + + def disconnect_client(self, client_socket: socket.socket) -> None: + try: + client = self.connected_clients[client_socket] + except KeyError: + log.debug(f"Ignoring disconnect request for untracked client: {client_socket} (already disconnected?)") + return + + log.info(f"Disconnecting {client}.") + del self.connected_clients[client_socket] + client_socket.close() + + def stop(self) -> None: + """Disconnect all clients and close the server socket connection.""" + for client_sock in self.connected_clients: + client_sock.close() + + self.socket.close() diff --git a/app/models/__init__.py b/zerocom/protocol/__init__.py similarity index 100% rename from app/models/__init__.py rename to zerocom/protocol/__init__.py diff --git a/zerocom/protocol/abc.py b/zerocom/protocol/abc.py new file mode 100644 index 0000000..d85043c --- /dev/null +++ b/zerocom/protocol/abc.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +import struct +from abc import ABC, abstractmethod +from itertools import count +from typing import Any, Optional, cast + +from zerocom.protocol.utils import enforce_range + + +class BaseWriter(ABC): + """Base class holding write buffer/connection interactions.""" + + __slots__ = () + + @abstractmethod + def write(self, data: bytes) -> None: + ... + + def _write_packed(self, fmt: str, *value: object) -> None: + """Write a value of given struct format in big-endian mode. + + Available formats are listed in struct module's docstring. + """ + self.write(struct.pack(">" + fmt, *value)) + + @enforce_range(typ="Byte (8-bit signed int)", byte_size=1, signed=True) + def write_byte(self, value: int) -> None: + """Write a single signed 8-bit integer. + + Signed 8-bit integers must be within the range of -128 and 127. Going outside this range will raise a + ValueError. + + Number is written in two's complement format. + """ + self._write_packed("b", value) + + @enforce_range(typ="Unsigned byte (8-bit unsigned int)", byte_size=1, signed=False) + def write_ubyte(self, value: int) -> None: + """Write a single unsigned 8-bit integer. + + Unsigned 8-bit integers must be within range of 0 and 255. Going outside this range will raise a ValueError. + """ + self._write_packed("B", value) + + def write_varint(self, value: int, *, max_size: Optional[int] = None) -> None: + """Write an arbitrarily big unsigned integer in a variable length format. + + This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. + + Will keep writing bytes until the value is depleted (fully sent). If `max_size` is specified, writing will be + limited up to integer values of max_size bytes, and trying to write bigger values will rase a ValueError. Note + that limiting to max_size of 4 (32-bit int) doesn't imply at most 4 bytes will be sent, and will in fact take 5 + bytes at most, due to the variable encoding overhead. + + Varnums use 7 least significant bits of each sent byte to encode the value, and the most significant bit to + indicate whether there is another byte after it. The least significant group is written first, followed by each + of the more significant groups, making varints little-endian, however in groups of 7 bits, not 8. + """ + # We can't use enforce_range as decorator directly, because our byte_size varies + # instead run it manually from here as a check function + _wrapper = enforce_range( + typ=f"{max_size if max_size else 'unlimited'}-byte unsigned varnum", + byte_size=max_size if max_size else None, + signed=False, + ) + _check_f = _wrapper(lambda self, value: None) + _check_f(self, value) + + remaining = value + while True: + if remaining & ~0x7F == 0: # final byte + self.write_ubyte(remaining) + return + # Write only 7 least significant bits, with the first being 1. + # first bit here represents that there will be another value after + self.write_ubyte(remaining & 0x7F | 0x80) + # Subtract the value we've already sent (7 least significant bits) + remaining >>= 7 + + def write_utf(self, value: str, max_varint_size: int = 2) -> None: + """Write a UTF-8 encoded string, prefixed with a varshort of it's size (in bytes). + + Will write n bytes, depending on the amount of bytes in the string + up to 3 bytes from prefix varshort, + holding this size (n). This means a maximum of 2**31-1 + 5 bytes can be written. + + Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the + worst case of 4 bytes per every character, at most 8192 characters can be written, however this number + will usually be much bigger (up to 4x) since it's unlikely each character would actually take up 4 bytes. (All + of the ASCII characters only take up 1 byte). + + If the given string is longer than this, ValueError will be raised for trying to write an invalid varshort. + """ + data = bytearray(value, "utf-8") + self.write_varint(len(value), max_size=max_varint_size) + self.write(data) + + def write_bytearray(self, data: bytearray) -> None: + """Write an arbitrary sequence of bytes, prefixed with a varint of it's size.""" + self.write_varint(len(data)) + self.write(data) + + +class BaseReader(ABC): + """Base class holding read buffer/connection interactions.""" + + __slots__ = () + + @abstractmethod + def read(self, length: int) -> bytearray: + ... + + def _read_unpacked(self, fmt: str) -> Any: # noqa: ANN401 + """Read bytes and unpack them into given struct format in big-endian mode. + + + The amount of bytes to read will be determined based on the format string automatically. + i.e.: With format of "iii" (referring to 3 signed 32-bit ints), the read length is set as 3x4 (since a signed + 32-bit int takes 4 bytes), making the total length to read 12 bytes, returned as Tuple[int, int, int] + + Available formats are listed in struct module's docstring. + """ + length = struct.calcsize(fmt) + data = self.read(length) + unpacked = struct.unpack(">" + fmt, data) + + if len(unpacked) == 1: + return unpacked[0] + return unpacked + + def read_byte(self) -> int: + """Read a single signed 8-bit integer. + + Will read 1 byte in two's complement format, getting int values between -128 and 127. + """ + return self._read_unpacked("b") + + def read_ubyte(self) -> int: + """Read a single unsigned 8-bit integer. + + Will read 1 byte, getting int value between 0 and 255 directly. + """ + return self._read_unpacked("B") + + def read_varint(self, *, max_size: Optional[int] = None) -> int: + """Read an arbitrarily big unsigned integer in a variable length format. + + This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. + + Will keep reading bytes until the value is depleted (fully sent). If `max_size` is specified, reading will be + limited up to integer values of max_size bytes, and trying to read bigger values will rase an IOError. Note + that limiting to max_size of 4 (32-bit int) doesn't imply at most 4 bytes will be sent, and will in fact take 5 + bytes at most, due to the variable encoding overhead. + + Varnums use 7 least significant bits of each sent byte to encode the value, and the most significant bit to + indicate whether there is another byte after it. The least significant group is written first, followed by each + of the more significant groups, making varints little-endian, however in groups of 7 bits, not 8. + """ + value_max = (1 << (max_size * 8)) - 1 if max_size else None + result = 0 + for i in count(): + byte = self.read_ubyte() + # Read 7 least significant value bits in this byte, and shift them appropriately to be in the right place + # then simply add them (OR) as additional 7 most significant bits in our result + result |= (byte & 0x7F) << (7 * i) + + # Ensure that we stop reading and raise an error if the size gets over the maximum + # (if the current amount of bits is higher than allowed size in bits) + if value_max and result > value_max: + max_size = cast(int, max_size) + raise IOError(f"Received varint was outside the range of {max_size}-byte ({max_size * 8}-bit) int.") + + # If the most significant bit is 0, we should stop reading + if not byte & 0x80: + break + + return result + + def read_utf(self, max_varint_size: int = 2) -> str: + """Read a UTF-8 encoded string, prefixed with a varshort of it's size (in bytes). + + Will read n bytes, depending on the prefix varint (amount of bytes in the string) + up to 3 bytes from prefix + varshort itself, holding this size (n). This means a maximum of 2**15-1 + 3 bytes can be read (and written). + + Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the + worst case of 4 bytes per every character, at most 8192 characters can be written, however this number + will usually be much bigger (up to 4x) since it's unlikely each character would actually take up 4 bytes. (All + of the ASCII characters only take up 1 byte). + """ + length = self.read_varint(max_size=max_varint_size) + bytes = self.read(length) + return bytes.decode("utf-8") + + def read_bytearray(self) -> bytearray: + """Read an arbitrary sequence of bytes, prefixed with a varint of it's size.""" + length = self.read_varint() + return self.read(length) diff --git a/zerocom/protocol/buffer.py b/zerocom/protocol/buffer.py new file mode 100644 index 0000000..210df6d --- /dev/null +++ b/zerocom/protocol/buffer.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from zerocom.protocol.abc import BaseReader, BaseWriter + + +class Buffer(BaseReader, BaseWriter, bytearray): + """Buffer implementation for BaseReader and BaseWriter via python's bytearrays. + + This class holds all basic interactions for writing/reading data into/from internal bytearray. + """ + + __slots__ = ("pos",) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pos = 0 + + def write(self, data: bytes) -> None: + """Write new data into the buffer.""" + self.extend(data) + + def read(self, length: int) -> bytearray: + """Read data stored in the buffer. + + Reading data doesn't remove that data, rather that data is treated as already read, and + next read will start from the first unread byte. If freeing the data is necessary, check the clear function. + + Trying to read more data than is available will raise an IOError, however it will deplete the remaining data + and the partial data that was read will be a part of the error message. This behavior is here to mimic reading + from a socket connection. + """ + end = self.pos + length + + if end > len(self): + data = self[self.pos : len(self)] + bytes_read = len(self) - self.pos + self.pos = len(self) + raise IOError( + "Requested to read more data than available." + f" Read {bytes_read} bytes: {data}, out of {length} requested bytes." + ) + + try: + return self[self.pos : end] + finally: + self.pos = end + + def clear(self, only_already_read: bool = False) -> None: + """ + Clear out the stored data and reset position. + + If `only_already_read` is True, only clear out the data which was already read, and reset the position. + This is mostly useful to avoid keeping large chunks of data in memory for no reason. + """ + if only_already_read: + del self[: self.pos] + else: + super().clear() + self.pos = 0 + + def reset(self) -> None: + """Reset the position in the buffer.""" + self.pos = 0 + + def flush(self) -> bytearray: + """Read all of the remaining data in the buffer and clear it out.""" + data = self[self.pos : len(self)] + self.clear() + return data diff --git a/zerocom/protocol/connection.py b/zerocom/protocol/connection.py new file mode 100644 index 0000000..5375ad6 --- /dev/null +++ b/zerocom/protocol/connection.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import socket +from typing import Generic, TypeVar + +from zerocom.protocol.abc import BaseReader, BaseWriter + +T_SOCK = TypeVar("T_SOCK", bound=socket.socket) + + +class SocketConnection(BaseReader, BaseWriter, Generic[T_SOCK]): + """Networked implementation for BaseReader and BaseWriter via python's sockets. + + This class holds all basic interactions for writing/reading data (i.e. sending/receiving) data via sockets. + """ + + def __init__(self, socket: T_SOCK): + self.socket = socket + + def read(self, length: int) -> bytearray: + result = bytearray() + while len(result) < length: + new = self.socket.recv(length - len(result)) + if len(new) == 0: + if len(result) == 0: + raise IOError("Server did not respond with any information.") + raise IOError(f"Server stopped responding (got {len(result)} bytes, but expected {length} bytes).") + result.extend(new) + + return result + + def write(self, data: bytes) -> None: + self.socket.send(data) + + def __del__(self): + self.socket.close() diff --git a/zerocom/protocol/utils.py b/zerocom/protocol/utils.py new file mode 100644 index 0000000..e0d0350 --- /dev/null +++ b/zerocom/protocol/utils.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from functools import wraps +from typing import Callable, Optional, TYPE_CHECKING, TypeVar, cast + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + P = ParamSpec("P") +else: + P = [] + +R = TypeVar("R") + + +def enforce_range(*, typ: str, byte_size: Optional[int], signed: bool) -> Callable: + """Decorator enforcing proper int value range, based on the number of max bytes (size). + + If a value is outside of the automatically determined allowed range, a ValueError will be raised, + showing the given `typ` along with the allowed range info. + + If the byte_size is None, infinite max size is assumed. Note that this is only possible with unsigned types, + since there's no point in enforcing infinite range. + """ + if byte_size is None: + if signed is True: + raise ValueError("Enforcing infinite byte-size for signed type doesn't make sense (includes all numbers).") + value_max = float("inf") + value_max_s = "infinity" + value_min = 0 + value_min_s = "0" + else: + if signed: + value_max = (1 << (byte_size * 8 - 1)) - 1 + value_max_s = f"{value_max} (2**{byte_size * 8 - 1} - 1)" + value_min = -1 << (byte_size * 8 - 1) + value_min_s = f"{value_min} (-2**{byte_size * 8 - 1})" + else: + value_max = (1 << (byte_size * 8)) - 1 + value_max_s = f"{value_max} (2**{byte_size * 8} - 1)" + value_min = 0 + value_min_s = "0" + + def wrapper(func: Callable[P, R]) -> Callable[P, R]: + @wraps(func) + def inner(*args: P.args, **kwargs: P.kwargs) -> R: + value = cast(int, args[1]) + if value > value_max or value < value_min: + raise ValueError(f"{typ} must be within {value_min_s} and {value_max_s}, got {value}.") + return func(*args, **kwargs) + + return inner + + return wrapper diff --git a/zerocom/server.py b/zerocom/server.py new file mode 100644 index 0000000..0e99453 --- /dev/null +++ b/zerocom/server.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import logging +import select +import sys +from typing import NoReturn + +from zerocom.config import Config +from zerocom.network.server import Server + +log = logging.getLogger(__name__) + + +def start_server(host: str, port: int) -> NoReturn: + server = Server((host, port)) + + while True: + try: + ready_to_read, _, in_error = select.select(server.socket_list, [], server.socket_list) + except KeyboardInterrupt: + log.info("Stopping the server...") + server.stop() + sys.exit(0) + + for socket_ in ready_to_read: + if socket_ is server.socket: + server.process_connection() + else: + server.process_message(socket_) + + for socket_ in in_error: + if socket_ is not server.socket: + server.disconnect_client(socket_) + else: + log.error("Server socket in error!") + + +if __name__ == "__main__": + start_server(Config.IP, Config.PORT) diff --git a/zerocom/utils/__init__.py b/zerocom/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zerocom/utils/log.py b/zerocom/utils/log.py new file mode 100644 index 0000000..14ca260 --- /dev/null +++ b/zerocom/utils/log.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import logging +import logging.handlers +from pathlib import Path + +from rich.logging import RichHandler + +import zerocom.config + +LOG_LEVEL = logging.DEBUG if zerocom.config.DEBUG else logging.INFO +LOG_FILE = zerocom.config.LOG_FILE +LOG_FILE_MAX_SIZE = zerocom.config.LOG_FILE_MAX_SIZE +LOG_FORMAT = "%(asctime)s | %(name)s | %(levelname)7s | %(message)s" + + +def setup_logging() -> None: + """Sets up logging library to use our log format and defines log levels.""" + root_log = logging.getLogger() + log_formatter = logging.Formatter(LOG_FORMAT, datefmt="%Y-%m-%d %H:%M:%S") + + rich_handler = RichHandler(show_time=False) + rich_handler.setFormatter(log_formatter) + root_log.addHandler(rich_handler) + + if LOG_FILE is not None: + file_handler = logging.handlers.RotatingFileHandler(Path(LOG_FILE), maxBytes=LOG_FILE_MAX_SIZE) + file_handler.setFormatter(log_formatter) + root_log.addHandler(file_handler) + + root_log.setLevel(LOG_LEVEL)