Compare commits

..

25 Commits
dev ... main

Author SHA1 Message Date
11d09b76df
test: Bring the MockSHSSocket testing class in line with the secret-handshake library 2023-11-19 07:42:25 +01:00
377368509e
build(typing): Add stricter type hinting to LocalMessage’s feed and previous attribute 2023-11-19 07:30:31 +01:00
59c99097a2
refactor: Use SHSDuplexStream.close instead of disconnect
`disconnect()` is now deprecated to make the API more consistent.
2023-11-19 07:28:06 +01:00
c2fffafa4e
feat: Create the PSHandler type 2023-11-19 07:26:23 +01:00
55abb6e341
ci: Enable (almost) all PyLint messages 2023-11-18 07:15:17 +01:00
526117ae18
chore: Remove useless PyLint suppressions 2023-11-18 07:15:17 +01:00
d6881cd8d5
feat: Process boolean True responses over Packet Stream 2023-11-18 07:07:12 +01:00
9b54ea6cec
chore: Remove unnecessary return type markup 2023-11-18 07:03:39 +01:00
df8c79514a
chore: Remove a useless super().__init__() call from PSStreamHandler 2023-11-18 07:01:14 +01:00
0c17410e18
style: Styling i forgot to commit earlier 2023-11-18 06:59:39 +01:00
a85fce7e3f
test: Fix test_packet_stream.py so there are no coverage issues in it 2023-11-18 06:40:35 +01:00
9e4a254127
test: Fully cover feed/models.py with tests 2023-11-18 06:40:35 +01:00
1c1e57d868
ci: Add and configure mypy, and make it happy 2023-11-16 09:44:36 +01:00
f2a54b5ce6
chore: Get rid of all __await__ methods 2023-11-16 06:12:12 +01:00
b30603a190
feat: Make it possible to load SSB secrets from a non-default file 2023-11-16 05:29:52 +01:00
c588967c11 build: Remove async_generator as a dependency
Python 3.6+ can do this natively.
2023-11-15 14:32:00 +01:00
01e8de96b2
fix: Check for empty messages before checking its properties in MuxRPCAPI 2023-11-15 06:13:09 +01:00
6ddb8bc8d3
test: Add tests for feed utils 2023-11-15 05:46:54 +01:00
69433cab86
refactor: Change MuxRPC.__await__ MuxRPC.process_messages
`__await__` can be a bit dangerous in the world of `asyncio`.
2023-11-15 05:36:29 +01:00
3b2c5cc792 test: Let mocker.path.object figure out the mock type to use in packet stream tests 2023-11-14 13:05:09 +01:00
d51f27d883
ci: Add and configure PyLint, and make it happy 2023-11-14 06:03:46 +01:00
e0cd456e77
chore: Remove the unused decorated function in MuxRPCAPI.define 2023-11-14 05:21:35 +01:00
8f5d355ea0
ci: Update and configure isort, and make it happy 2023-11-14 05:16:10 +01:00
d1a0510734
ci: Use black instead of flake8
It results in mostly the same style, plus it’s configurable via pyproject.toml.
2023-11-13 13:57:42 +01:00
50442c56fe
chore: Make the project REUSE compatible 2023-11-13 13:41:30 +01:00
19 changed files with 767 additions and 242 deletions

View File

@ -37,3 +37,16 @@ repos:
language: system
require_serial: true
types_or: [python, pyi]
- id: mypy
name: mypy
entry: poetry run mypy
args: ["--strict"]
language: system
types_or: [python, pyi]
require_serial: true
- id: reuse
name: reuse
entry: poetry run reuse
args: ["lint"]
language: system
pass_filenames: false

8
.reuse/dep5 Normal file
View File

@ -0,0 +1,8 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: PySSB
Upstream-Contact: Pedro Ferreira <pedro@dete.st>
Source: https://github.com/pferreir/pyssb
Files: AUTHORS README.rst poetry.lock .gitignore .pre-commit-config.yaml
Copyright: 2023 Gergely Polonkai <python-ssb@gergely.polonkai.eu>
License: CC0-1.0

19
LICENSE
View File

@ -1,19 +0,0 @@
Copyright (c) 2017 pyssb contributors (see AUTHORS for more details)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

121
LICENSES/CC0-1.0.txt Normal file
View File

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

9
LICENSES/MIT.txt Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +1,25 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Example SSB Client"""
from asyncio import ensure_future, gather, get_event_loop
@ -39,13 +61,23 @@ async def test_client() -> None:
async for msg in api.call(
"createHistoryStream",
[{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}],
[
{
"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
"seq": 1,
"live": False,
"keys": False,
}
],
"source",
):
print("> RESPONSE:", msg)
try:
print("> RESPONSE:", await api.call("whoami", [], "sync"))
response_handler = api.call("whoami", [], "sync")
response = await response_handler.get_response()
print("> RESPONSE:", response)
except MuxRPCAPIException as e:
print(e)
@ -80,7 +112,7 @@ async def main(keypair: SigningKey) -> None:
packet_stream = PacketStream(client)
await client.open()
api.add_connection(packet_stream)
await gather(ensure_future(api), test_client())
await gather(ensure_future(api.process_messages()), test_client())
if __name__ == "__main__":

View File

@ -1,3 +1,25 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Test SSB server"""
from asyncio import get_event_loop
@ -28,6 +50,7 @@ async def on_connect(conn: SHSDuplexStream) -> None:
async def main() -> None:
"""The main function to run"""
server = SHSServer("127.0.0.1", 8008, load_ssb_secret()["keypair"])
server.on_connect(on_connect)
await server.listen()

196
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
[[package]]
name = "alabaster"
@ -39,6 +39,17 @@ files = [
[package.dependencies]
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "async-generator"
version = "1.8"
description = "Async generators for Python 3.5"
optional = false
python-versions = "*"
files = [
{file = "async_generator-1.8-py3-none-any.whl", hash = "sha256:d9253336202cb9df50ba617893fe794c61394a7eb4b9054f285c860f395ac6ff"},
{file = "async_generator-1.8.zip", hash = "sha256:928b644cfc92be498f2d6c431e0082ae79ea736fbdf1ce4247881071dd525348"},
]
[[package]]
name = "babel"
version = "2.13.1"
@ -58,29 +69,29 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
version = "23.10.1"
version = "23.11.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"},
{file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"},
{file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"},
{file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"},
{file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"},
{file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"},
{file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"},
{file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"},
{file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"},
{file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"},
{file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"},
{file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"},
{file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"},
{file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"},
{file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"},
{file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"},
{file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"},
{file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"},
{file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
{file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
{file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
{file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
{file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
{file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
{file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
{file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
{file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
{file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
{file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
{file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
{file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
{file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
{file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
{file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
{file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
{file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
]
[package.dependencies]
@ -640,38 +651,38 @@ files = [
[[package]]
name = "mypy"
version = "1.6.1"
version = "1.7.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"},
{file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"},
{file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"},
{file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"},
{file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"},
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
{file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"},
{file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"},
{file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"},
{file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"},
{file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"},
{file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"},
{file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"},
{file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"},
{file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"},
{file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"},
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
{file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"},
{file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"},
{file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"},
{file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"},
{file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"},
{file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"},
{file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"},
{file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"},
{file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"},
{file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"},
{file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"},
{file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"},
{file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"},
{file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"},
{file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"},
{file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"},
{file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"},
{file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"},
{file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"},
{file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"},
{file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"},
{file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"},
{file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"},
{file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"},
{file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"},
{file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"},
{file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"},
]
[package.dependencies]
@ -682,6 +693,7 @@ typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
@ -730,13 +742,13 @@ files = [
[[package]]
name = "platformdirs"
version = "3.11.0"
version = "4.0.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
{file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"},
{file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"},
]
[package.extras]
@ -829,29 +841,47 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pynacl"
version = "1.5.0"
version = "1.1.2"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
python-versions = ">=3.6"
python-versions = "*"
files = [
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
{file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
{file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
{file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
{file = "PyNaCl-1.1.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:9558ef5c1ae45322c054d1d1151016e0463b4da8b5c746a675e99c5c7d8f4faa"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:301c966c1e17950e50d174ab4b2e7ef3e98ff51ad7a591152a19fe2139281eed"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d21d733a63637ddf41d0cab50135ec9f5224dd22fd10ebf5c5f5f946b833f84"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:c93d151efcdd7d214b1b11d781c9f1b125f0208cd06d9762bddabdfeac1cedfc"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1b4938a557b32e5c6b27fac79a94cf1abb70753b5462a0b577bd2a77e09dacd0"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:99f91eb80b85fe16f70d362cfeae8eeeb108cd09a85f039fdab02164762f764b"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57314a7bad4bd39501dc622942f9921923673e52e126b0fc4f0214b5d25d619a"},
{file = "PyNaCl-1.1.2-cp33-cp33m-macosx_10_6_intel.whl", hash = "sha256:506bc2591968a1a7b6577075bc29a591d8fff5bdfec03b0dd926f34b75b670e5"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:4c15d7cea1a313fff3f68222e682ee1f855e43c0865081cad7385066a6b57d75"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:c4ea0e3b9f3317ada56e12c7b37f6d0316900ae8b54a20d7b100d4e14350ac87"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win32.whl", hash = "sha256:53d83faf274813a5778bba1cd4cb96b79f39e44a63b1c4a4dada01a2b0eeafe8"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win_amd64.whl", hash = "sha256:5172395dea8203ae124fd282fef3d242aa75366d66aebc0f5aab0c4753eed97b"},
{file = "PyNaCl-1.1.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:877879903cddb5da317fe86d923f65eb28c62fd7feb79cd3402d166e401f9423"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:123c41df1db119397f2e26e9c63ca2ea853d3663e26b1c389bd3859dc1b7178a"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:ceb16b7977123713ad898450ca86a2dc6706a17fe4cf278ffb6b76929c186550"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win32.whl", hash = "sha256:813d4170f62d68236bb041cf731e8d1f34fc1006a5e5d81139bead6ddaa9d169"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win_amd64.whl", hash = "sha256:f01405a5c453b866e35338c53882f7ba7069c1f4e4045ce67513ad45c796f8a5"},
{file = "PyNaCl-1.1.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:4a3be9f884df08087996516707446ba55648bbefae8428bf578fa05f20fa2ed9"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d14f18f8bc43977691276097524b9713d21b9635fea9791311261a66e4fe296"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9532aaa446840ece574c719ad3bbf25f60ca9871f48b5446e3f73e8b498e2398"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:67b75a950dbc4025bfa549c183baa17db4096955912f385df31830e5a2121974"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:dfc85c2b414dee700e32764559d560063825ec1470d3ee6c973e43c80a622e56"},
{file = "PyNaCl-1.1.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3fd984580cbea8e02fc531aa32ab9487b72c30127f9e4c8db9ba3fe8950ecc93"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:75a427377b2088c29a44db08c796c75a9cde2f9725dd041903cfbc0f6034895c"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ca2deb968135f1400105ca902f5cef24ba6984b6a4904756498afcb9077c76f9"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:ffb74ac578b3b80b1d2d5a23a6dd7b1d6682e5fce6a7b3d21b46b180a5546055"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b796d95704b674100bd99fc42bbde9f8f2ccddae8599a4d4bbcb518428dfbfed"},
{file = "PyNaCl-1.1.2.tar.gz", hash = "sha256:32f52b754abf07c319c04ce16905109cab44b0e7f7c79497431d3b2000f8af8c"},
]
[package.dependencies]
cffi = ">=1.4.1"
six = "*"
[package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
tests = ["pytest"]
[[package]]
name = "pytest"
@ -1024,21 +1054,22 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "secret-handshake"
version = "0.1.0"
description = "A module that implements Secret Handshake"
version = "0.1.0.dev3"
description = "A module that implements Secret Handshake as specified in \"Designing a Secret Handshake: Authenticated"
optional = false
python-versions = "^3.9"
files = []
develop = false
python-versions = "*"
files = [
{file = "secret-handshake-0.1.0.dev3.tar.gz", hash = "sha256:be1f812101c0eb84a82a08d119090d8f423230878e233b4bfc551fb708b4e32a"},
]
[package.dependencies]
PyNaCl = "^1.5.0"
async-generator = "1.8"
pynacl = "1.1.2"
[package.source]
type = "git"
url = "https://gitea.polonkai.eu/gergely/PySecretHandshake"
reference = "main"
resolved_reference = "5a3af659277219536eeef4c64b8a991902f0acd2"
[package.extras]
all = ["Sphinx (>=1.6.2)", "check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"]
docs = ["Sphinx (>=1.6.2)"]
tests = ["check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"]
[[package]]
name = "setuptools"
@ -1077,6 +1108,17 @@ files = [
{file = "simplejson-3.16.0.tar.gz", hash = "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5"},
]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "snowballstemmer"
version = "2.2.0"
@ -1337,4 +1379,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "d80cbfdf7923c50c95505a84d8ad75eae016ca81ae32a8b22d074569b0a0fcbd"
content-hash = "98384046072d2dd4f649a93231ee6a84e5b21be34f15d5d2196cd3832f15ebca"

View File

@ -1,3 +1,6 @@
# SPDX-License-Identifier: MIT
#
# SPDX-Copyright-Text: © 2017 PySSB contributors (see AUTHORS for more details)
[tool.poetry]
name = "ssb"
version = "0.1.0"
@ -5,28 +8,29 @@ description = "Secure Scuttlebutt library in Python"
authors = ["PyScuttleButt Contributors <pedro@dete.st>"]
license = "MIT"
readme = "README.rst"
include = ["ssb/py.typed"]
[tool.poetry.dependencies]
python = "^3.9"
PyNaCl = "^1.1.0"
PyYAML = "^6.0.1"
secret-handshake = { git = "https://gitea.polonkai.eu/gergely/PySecretHandshake", branch = "main" }
secret-handshake = { version = "0.1.0.dev3", allow-prereleases = true }
simplejson = "3.16.0"
colorlog = "^6.7.0"
[tool.poetry.group.dev.dependencies]
black = "^23.10.1"
check-manifest = "^0.39"
commitizen = "^3.12.0"
coverage = "^7.3.2"
isort = "^5.12.0"
mypy = "^1.6.1"
pep257 = "^0.7.0"
pylint = "^3.0.2"
pytest = "^7.4.3"
pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0"
pytest-mock = "^3.12.0"
commitizen = "^3.12.0"
black = "^23.10.1"
pylint = "^3.0.2"
mypy = "^1.6.1"
types-pyyaml = "^6.0.12.12"
types-simplejson = "^3.19.0.2"
@ -50,6 +54,10 @@ force_sort_within_sections = true
line_length = 120
profile = "black"
[tool.pylint.messages_control]
enable = ["all"]
disable = ["locally-disabled", "suppressed-message"]
[tool.pylint.format]
max-line-length = 120

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length=120

View File

@ -1,3 +1,25 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Feed related functionality"""
from .models import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException

View File

@ -1,8 +1,30 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Feed models"""
from base64 import b64encode
from collections import OrderedDict, namedtuple
import datetime
from datetime import datetime
from hashlib import sha256
from typing import Any, Dict, Optional
@ -30,7 +52,7 @@ def to_ordered(data: Dict[str, Any]) -> OrderedDict[str, Any]:
def get_millis_1970() -> int:
"""Get the UNIX timestamp in milliseconds"""
return int(datetime.datetime.utcnow().timestamp() * 1000)
return int(datetime.utcnow().timestamp() * 1000)
class Feed:
@ -45,7 +67,7 @@ class Feed:
return tag(self.public_key).decode("ascii")
def sign(self, msg: "Message") -> bytes:
def sign(self, msg: bytes) -> bytes:
"""Sign a message"""
raise NoPrivateKeyException("Cannot use remote identity to sign (no private key!)")
@ -55,7 +77,7 @@ class LocalFeed(Feed):
"""Class representing a local feed"""
def __init__(self, private_key: SigningKey): # pylint: disable=super-init-not-called
self.private_key: SigningKey = private_key
self.private_key = private_key
@property
def public_key(self) -> VerifyKey:
@ -64,10 +86,10 @@ class LocalFeed(Feed):
return self.private_key.verify_key
@public_key.setter
def public_key(self, _: VerifyKey) -> None:
raise TypeError("Cannot set just the public key of a local feed")
def public_key(self, key: VerifyKey) -> None:
raise TypeError("Can not set only the public key for a local feed")
def sign(self, msg: "Message") -> bytes:
def sign(self, msg: bytes) -> bytes:
"""Sign a message for this feed"""
return self.private_key.sign(msg).signature
@ -87,20 +109,20 @@ class Message:
):
self.feed = feed
self.content = content
if signature is None:
raise ValueError("signature can't be None")
self.signature = signature
self.previous = previous
self.timestamp = get_millis_1970() if timestamp is None else timestamp
if self.previous:
self.sequence: int = self.previous.sequence + 1
else:
self.sequence = sequence
self.timestamp = get_millis_1970() if timestamp is None else timestamp
self._check_signature()
def _check_signature(self) -> None:
if self.signature is None:
raise ValueError("signature can't be None")
@classmethod
def parse(cls, data: bytes, feed: Feed) -> Self:
@ -158,30 +180,23 @@ class Message:
class LocalMessage(Message):
"""Class representing a local message"""
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
feed: LocalFeed
previous: "LocalMessage"
def __init__( # pylint: disable=too-many-arguments
self,
feed: Feed,
feed: LocalFeed,
content: Dict[str, Any],
signature: Optional[str] = None,
sequence: int = 1,
timestamp: Optional[int] = None,
previous: Optional[Message] = None,
previous: Optional["LocalMessage"] = None,
):
self.feed = feed
self.content = content
super().__init__(feed, content, signature=signature, sequence=sequence, timestamp=timestamp, previous=previous)
self.previous = previous
if self.previous:
self.sequence = self.previous.sequence + 1
else:
self.sequence = sequence
self.timestamp = get_millis_1970() if timestamp is None else timestamp
if signature is None:
def _check_signature(self) -> None:
if self.signature is None:
self.signature = self._sign()
else:
self.signature = signature
def _sign(self) -> str:
# ensure ordering of keys and indentation of 2 characters, like ssb-keys

View File

@ -1,6 +1,28 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""MuxRPC"""
from typing import Any, AsyncIterator, Callable, Dict, Generator, List, Literal, Optional, Union
from typing import Any, AsyncIterator, Callable, Dict, List, Literal, Optional, Union
from typing_extensions import Self
@ -16,7 +38,7 @@ class MuxRPCAPIException(Exception):
"""Exception to raise on MuxRPC API errors"""
class MuxRPCHandler: # pylint: disable=too-few-public-methods
class MuxRPCHandler:
"""Base MuxRPC handler class"""
def check_message(self, msg: PSMessage) -> None:
@ -27,9 +49,6 @@ class MuxRPCHandler: # pylint: disable=too-few-public-methods
if isinstance(body, dict) and "name" in body and body["name"] == "Error":
raise MuxRPCAPIException(body["message"])
def __await__(self) -> Generator[Optional[PSMessage], None, None]:
raise NotImplementedError()
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
raise NotImplementedError()
@ -41,28 +60,30 @@ class MuxRPCHandler: # pylint: disable=too-few-public-methods
raise NotImplementedError()
async def get_response(self) -> PSMessage:
"""Get the response for an RPC request"""
raise NotImplementedError()
class MuxRPCRequestHandler(MuxRPCHandler): # pylint: disable=abstract-method
"""Base class for MuxRPC request handlers"""
"""MuxRPC handler for incoming RPC requests"""
def __init__(self, ps_handler: PSRequestHandler):
self.ps_handler = ps_handler
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
return self
async def get_response(self) -> PSMessage:
"""Get the response data"""
async def __anext__(self) -> Optional[PSMessage]:
msg = await self.ps_handler.__anext__()
assert msg
self.check_message(msg)
return msg
class MuxRPCSourceHandler(MuxRPCHandler): # pylint: disable=abstract-method
"""MuxRPC handler for sources"""
"""MuxRPC handler for source-type RPC requests"""
def __init__(self, ps_handler: PSStreamHandler):
self.ps_handler = ps_handler
@ -83,14 +104,12 @@ class MuxRPCSourceHandler(MuxRPCHandler): # pylint: disable=abstract-method
class MuxRPCSinkHandlerMixin: # pylint: disable=too-few-public-methods
"""Mixin for sink-type MuxRPC handlers"""
connection: Optional[PacketStream]
req: Optional[int]
connection: PacketStream
req: int
def send(self, msg: Any, msg_type: PSMessageType = PSMessageType.JSON, end: bool = False) -> None:
"""Send a message through the stream"""
assert self.connection
self.connection.send(msg, stream=True, msg_type=msg_type, req=self.req, end_err=end)
@ -144,6 +163,8 @@ class MuxRPCRequest:
body = message.body
assert isinstance(body, dict)
return cls(".".join(body["name"]), body["args"])
def __init__(self, name: str, args: List[MuxRPCRequestParam]):
@ -163,39 +184,34 @@ class MuxRPCMessage:
return cls(message.body)
def __init__(self, body: PSMessage):
def __init__(self, body: Union[bytes, str, Dict[str, Any], bool]):
self.body = body
def __repr__(self) -> str:
return f"<MuxRPCMessage {self.body}>"
return f"<MuxRPCMessage {self.body!r}>"
class MuxRPCAPI:
"""Generit MuxRPC API"""
"""Generic MuxRPC API"""
def __init__(self) -> None:
self.handlers: Dict[str, MuxRPCRequestHandlerType] = {}
self.connection: Optional[PacketStream] = None
def __aiter__(self) -> AsyncIterator[None]:
return self
async def process_messages(self) -> None:
"""Continuously process incoming messages"""
async def __anext__(self) -> None:
assert self.connection
req_message = await self.connection.__anext__()
async for req_message in self.connection:
if req_message is None:
raise StopAsyncIteration()
return
body = req_message.body
if isinstance(body, dict) and body.get("name"):
self.process(self.connection, MuxRPCRequest.from_message(req_message))
def __await__(self) -> Generator[None, None, None]:
yield from self.__anext__().__await__()
def add_connection(self, connection: PacketStream) -> None:
"""Set the packet stream connection of this RPC API"""

View File

@ -1,3 +1,25 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Packet streams"""
from asyncio import Event, Queue
@ -30,7 +52,7 @@ class PSStreamHandler:
def __init__(self, req: int):
self.req = req
self.queue: Queue[Optional["PSMessage"]] = Queue()
self.queue: Queue["PSMessage"] = Queue()
async def process(self, msg: "PSMessage") -> None:
"""Process a pending message"""
@ -40,7 +62,9 @@ class PSStreamHandler:
async def stop(self) -> None:
"""Stop a pending request"""
await self.queue.put(None)
# We use the None value internally to signal __anext__ that the stream can be closed. It is not used otherwise,
# hence the typing ignore
await self.queue.put(None) # type: ignore[arg-type]
def __aiter__(self) -> AsyncIterator[Optional["PSMessage"]]:
return self
@ -60,7 +84,7 @@ class PSRequestHandler:
def __init__(self, req: int):
self.req = req
self.event = Event()
self._msg: Optional[PSMessage] = None
self._msg: Optional["PSMessage"] = None
async def process(self, msg: "PSMessage") -> None:
"""Process a message request"""
@ -74,13 +98,15 @@ class PSRequestHandler:
if not self.event.is_set():
self.event.set()
def __aiter__(self):
def __aiter__(self) -> AsyncIterator["PSMessage"]:
return self
async def __anext__(self) -> Optional["PSMessage"]:
async def __anext__(self) -> "PSMessage":
# wait until 'process' is called
await self.event.wait()
assert self._msg
return self._msg
@ -94,22 +120,28 @@ class PSMessage:
type_ = PSMessageType(flags & 0x03)
if type_ == PSMessageType.TEXT:
body_s = body.decode("utf-8")
decoded_body: Union[str, Dict[str, Any], bytes] = body.decode("utf-8")
elif type_ == PSMessageType.JSON:
body_s = simplejson.loads(body)
decoded_body = simplejson.loads(body)
else:
decoded_body = body
return cls(type_, body_s, bool(flags & 0x08), bool(flags & 0x04), req=req)
return cls(type_, decoded_body, bool(flags & 0x08), bool(flags & 0x04), req=req)
@property
def data(self) -> bytes:
"""The raw message data"""
if self.body is True:
return b"true"
if self.type == PSMessageType.TEXT:
assert isinstance(self.body, str)
return self.body.encode("utf-8")
if self.type == PSMessageType.JSON:
assert isinstance(self.body, dict)
return simplejson.dumps(self.body).encode("utf-8")
assert isinstance(self.body, bytes)
@ -117,7 +149,12 @@ class PSMessage:
return self.body
def __init__(
self, type_: PSMessageType, body: Any, stream: bool, end_err: bool, req: Optional[int] = None
self,
type_: PSMessageType,
body: Union[bytes, str, Dict[str, Any], bool],
stream: bool,
end_err: bool,
req: Optional[int] = None,
): # pylint: disable=too-many-arguments
self.stream = stream
self.end_err = end_err
@ -126,10 +163,13 @@ class PSMessage:
self.req = req
def __repr__(self) -> str:
if self.type == PSMessageType.BUFFER:
if self.body is True:
body = "EOF"
elif self.type == PSMessageType.BUFFER:
assert isinstance(self.body, bytes)
body = f"{len(self.body)} bytes"
else:
body = self.body
body = str(self.body)
req = "" if self.req is None else f" [{self.req}]"
is_stream = "~" if self.stream else ""
@ -161,24 +201,18 @@ class PacketStream:
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
return self
async def __anext__(self) -> Optional[PSMessage]:
async def __anext__(self) -> PSMessage:
while True:
msg = await self.read()
if not msg:
raise StopAsyncIteration()
if msg.req is not None and msg.req >= 0:
logger.info("RECV: %r", msg)
return msg
return None
async def __await__(self) -> None:
async for data in self:
logger.info("RECV: %r", data)
if data is None:
return
async def _read(self) -> Optional[PSMessage]:
try:
header = await self.connection.read()
@ -187,15 +221,18 @@ class PacketStream:
return None
flags, length, req = struct.unpack(">BIi", header)
n_packets = ceil(length / 4096)
body = b""
for _ in range(n_packets):
read_data = await self.connection.read()
if read_data is not None:
if not read_data:
logger.debug("DISCONNECT")
self.connection.close()
return None
body += read_data
logger.debug("READ %s %s", header, len(body))
@ -203,7 +240,7 @@ class PacketStream:
return PSMessage.from_header_body(flags, req, body)
except StopAsyncIteration:
logger.debug("DISCONNECT")
self.connection.disconnect()
self.connection.close()
return None
@ -231,7 +268,10 @@ class PacketStream:
def _write(self, msg: PSMessage) -> None:
logger.info("SEND [%d]: %r", msg.req, msg)
header = struct.pack(
">BIi", (int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value, len(msg.data), msg.req
">BIi",
(int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value,
len(msg.data),
msg.req,
)
self.connection.write(header)
self.connection.write(msg.data)
@ -240,7 +280,7 @@ class PacketStream:
def send( # pylint: disable=too-many-arguments
self,
data: Any,
data: Union[bytes, str, Dict[str, Any]],
msg_type: PSMessageType = PSMessageType.JSON,
stream: bool = False,
end_err: bool = False,
@ -275,4 +315,4 @@ class PacketStream:
"""Disconnect the stream"""
self._connected = False
self.connection.disconnect()
self.connection.close()

0
ssb/py.typed Normal file
View File

View File

@ -1,15 +1,37 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Utility functions"""
from base64 import b64decode, b64encode
import os
from typing import TypedDict
from typing import Optional, TypedDict
from nacl.signing import SigningKey, VerifyKey
import yaml
class SSBSecret(TypedDict):
"""Dictionary type to hold an SSB secret identity"""
"""Dictionary to hold an SSB identity"""
keypair: SigningKey
id: str
@ -20,15 +42,17 @@ class ConfigException(Exception):
def tag(key: VerifyKey) -> bytes:
"""Create tag from public key."""
"""Create tag from public key"""
return b"@" + b64encode(bytes(key)) + b".ed25519"
def load_ssb_secret() -> SSBSecret:
"""Load SSB keys from ~/.ssb"""
def load_ssb_secret(filename: Optional[str] = None) -> SSBSecret:
"""Load SSB keys from ``filename`` or, if unset, from ``~/.ssb/secret``"""
with open(os.path.expanduser("~/.ssb/secret"), encoding="utf-8") as f:
filename = filename or os.path.expanduser("~/.ssb/secret")
with open(filename, encoding="utf-8") as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
if config["curve"] != "ed25519":

View File

@ -1,12 +1,37 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Tests for the feed functionality"""
from base64 import b64decode
from collections import OrderedDict
from datetime import datetime, timezone
from nacl.signing import SigningKey, VerifyKey
import pytest
from pytest_mock import MockerFixture
from ssb.feed import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
from ssb.feed.models import get_millis_1970
SERIALIZED_M1 = b"""{
"previous": null,
@ -24,7 +49,7 @@ SERIALIZED_M1 = b"""{
}"""
@pytest.fixture
@pytest.fixture()
def local_feed() -> LocalFeed:
"""Fixture providing a local feed"""
@ -32,7 +57,7 @@ def local_feed() -> LocalFeed:
return LocalFeed(SigningKey(secret))
@pytest.fixture
@pytest.fixture()
def remote_feed() -> Feed:
"""Fixture providing a remote feed"""
@ -50,6 +75,17 @@ def test_local_feed() -> None:
assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
def test_local_feed_set_pubkey(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test setting only the public key for a local feed"""
key = SigningKey.generate().verify_key
with pytest.raises(TypeError) as ctx:
local_feed.public_key = key
assert str(ctx.value) == "Can not set only the public key for a local feed"
def test_remote_feed() -> None:
"""Test a remote feed"""
@ -66,7 +102,7 @@ def test_remote_feed() -> None:
)
with pytest.raises(NoPrivateKeyException):
feed.sign(m1)
feed.sign(m1.serialize())
def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
@ -80,15 +116,21 @@ def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefi
assert m1.timestamp == 1495706260190
assert m1.previous is None
assert m1.sequence == 1
assert m1.signature == (
"lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
assert (
m1.signature
== "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
)
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
m2 = LocalMessage(
local_feed,
OrderedDict(
[("type", "about"), ("about", local_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
[
("type", "about"),
("about", local_feed.id),
("name", "morpheus"),
("description", "Dude with big jaw"),
]
),
previous=m1,
timestamp=1495706447426,
@ -96,8 +138,9 @@ def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefi
assert m2.timestamp == 1495706447426
assert m2.previous is m1
assert m2.sequence == 2
assert m2.signature == (
"3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
assert (
m2.signature
== "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
)
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
@ -168,3 +211,61 @@ def test_parse(local_feed: LocalFeed) -> None: # pylint: disable=redefined-oute
m1 = LocalMessage.parse(SERIALIZED_M1, local_feed)
assert m1.content == {"type": "about", "about": local_feed.id, "name": "neo", "description": "The Chosen One"}
assert m1.timestamp == 1495706260190
def test_local_unsigned(local_feed: LocalFeed, mocker: MockerFixture) -> None: # pylint: disable=redefined-outer-name
"""Test creating an unsigned message on a local feed"""
mocked_dt = mocker.Mock(spec=datetime)
mocked_dt.utcnow = mocker.MagicMock(return_value=datetime(2023, 3, 7, 11, 45, 54, 0, tzinfo=timezone.utc))
mocker.patch("ssb.feed.models.datetime", mocked_dt)
msg = LocalMessage(local_feed, OrderedDict({"test": True}))
assert msg.feed == local_feed
assert msg.content == {"test": True}
assert msg.sequence == 1
assert msg.previous is None
assert msg.timestamp == 1678189554000
assert msg.signature == (
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
)
def test_local_signed(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test creating a signed message on a local feed"""
msg = LocalMessage(
local_feed,
OrderedDict({"test": True}),
timestamp=1678189554000,
signature=(
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
),
)
assert msg.feed == local_feed
assert msg.content == {"test": True}
assert msg.sequence == 1
assert msg.previous is None
assert msg.timestamp == 1678189554000
assert msg.signature == (
"WjkA5rjzsYDHqeavEPcbNAbRMp5NRFDBNATMWgcsccso8sfwhaWnIEvQW79fA5YgKKybzlIsCMWHherToEI2DA==.sig.ed25519"
)
@pytest.mark.parametrize(
"timestamp,expected",
(
(datetime(2023, 3, 7, 11, 45, 54, 0, tzinfo=timezone.utc), 1678189554000),
(datetime(2013, 5, 2, 2, 3, 4, 567890, tzinfo=timezone.utc), 1367460184567),
),
)
def test_millis(timestamp: datetime, expected: int, mocker: MockerFixture) -> None:
"""Test the get_millis_1970() function"""
mocked_dt = mocker.Mock(spec=datetime)
mocked_dt.utcnow = mocker.MagicMock(return_value=timestamp)
mocker.patch("ssb.feed.models.datetime", mocked_dt)
assert get_millis_1970() == expected

View File

@ -1,9 +1,31 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Tests for the packet stream"""
from asyncio import Event, ensure_future, gather
from asyncio.events import AbstractEventLoop
import json
from typing import AsyncGenerator, Awaitable, Callable, Generator, List
from typing import AsyncIterator, Awaitable, Callable, Generator, List, Optional
import pytest
from pytest_mock import MockerFixture
@ -12,10 +34,10 @@ from secret_handshake.network import SHSDuplexStream
from ssb.packet_stream import PacketStream, PSMessage, PSMessageType
async def _collect_messages(generator: AsyncGenerator[PSMessage, None]) -> List[PSMessage]:
async def _collect_messages(generator: AsyncIterator[Optional[PSMessage]]) -> List[Optional["PSMessage"]]:
results = []
async for msg in generator:
async for msg in generator: # pragma: no branch
results.append(msg)
return results
@ -44,12 +66,12 @@ MSG_BODY_2 = (
class MockSHSSocket(SHSDuplexStream):
"""A mocked SHS socket"""
def __init__(self): # pylint: disable=unused-argument
def __init__(self) -> None:
super().__init__()
self.input: List[bytes] = []
self.output: List[bytes] = []
self.is_connected = False
self.is_connected: bool = False
self._on_connect: List[Callable[[SHSDuplexStream], Awaitable[None]]] = []
def on_connect(self, cb: Callable[[SHSDuplexStream], Awaitable[None]]) -> None:
@ -61,7 +83,7 @@ class MockSHSSocket(SHSDuplexStream):
"""Read data from the socket"""
if not self.input:
raise StopAsyncIteration
raise StopAsyncIteration()
return self.input.pop(0)
@ -84,11 +106,6 @@ class MockSHSSocket(SHSDuplexStream):
yield self.output.pop(0)
def disconnect(self) -> None:
"""Disconnect from the remote party"""
self.is_connected = False
class MockSHSClient(MockSHSSocket):
"""A mocked SHS client"""
@ -98,9 +115,6 @@ class MockSHSClient(MockSHSSocket):
self.is_connected = True
for cb in self._on_connect:
await cb(self)
class MockSHSServer(MockSHSSocket):
"""A mocked SHS server"""
@ -163,11 +177,17 @@ async def test_message_decoding(ps_client: MockSHSClient) -> None: # pylint: di
messages = await _collect_messages(ps)
assert len(messages) == 1
assert messages[0]
assert messages[0].type == PSMessageType.JSON
assert messages[0].body == {
"name": ["createHistoryStream"],
"args": [
{"id": "@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519", "seq": 10, "live": True, "keys": False}
{
"id": "@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519",
"seq": 10,
"live": True,
"keys": False,
}
],
"type": "source",
}
@ -187,7 +207,12 @@ async def test_message_encoding(ps_client: MockSHSClient) -> None: # pylint: di
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
{
"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
"seq": 1,
"live": False,
"keys": False,
}
],
"type": "source",
},
@ -200,14 +225,21 @@ async def test_message_encoding(ps_client: MockSHSClient) -> None: # pylint: di
assert json.loads(body.decode("utf-8")) == {
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
{
"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
"seq": 1,
"live": False,
"keys": False,
}
],
"type": "source",
}
@pytest.mark.asyncio
async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture): # pylint: disable=redefined-outer-name
async def test_message_stream(
ps_client: MockSHSClient, mocker: MockerFixture # pylint: disable=redefined-outer-name
) -> None:
"""Test requesting a history stream"""
await ps_client.connect()
@ -221,7 +253,12 @@ async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture):
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
{
"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
"seq": 1,
"live": False,
"keys": False,
}
],
"type": "source",
},
@ -229,9 +266,8 @@ async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture):
)
assert ps.req_counter == 2
assert ps.register_handler.call_count == 1 # pylint: disable=no-member
assert ps.register_handler.call_count == 1 # type: ignore[attr-defined] # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mock_process = mocker.patch.object(handler, "process")
ps_client.feed([b"\n\x00\x00\x02\xc5\xff\xff\xff\xff", MSG_BODY_1])
@ -239,6 +275,8 @@ async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture):
assert mock_process.await_count == 1
# responses have negative req
assert msg
assert isinstance(msg.body, dict)
assert msg.req == -1
assert msg.body["previous"] == "%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256"
@ -248,7 +286,12 @@ async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture):
{
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
{
"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
"seq": 1,
"live": False,
"keys": False,
}
],
"type": "source",
},
@ -256,25 +299,31 @@ async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture):
)
assert ps.req_counter == 3
assert ps.register_handler.call_count == 2 # pylint: disable=no-member
assert ps.register_handler.call_count == 2 # type: ignore[attr-defined] # pylint: disable=no-member
handler = list(ps._event_map.values())[1][1] # pylint: disable=protected-access
mock_process = mocker.patch.object(handler, "process", wraps=handler.process)
ps_client.feed(
[b"\n\x00\x00\x02\xc5\xff\xff\xff\xfe", MSG_BODY_1, b"\x0e\x00\x00\x023\xff\xff\xff\xfe", MSG_BODY_2]
[
b"\n\x00\x00\x02\xc5\xff\xff\xff\xfe",
MSG_BODY_1,
b"\x0e\x00\x00\x023\xff\xff\xff\xfe",
MSG_BODY_2,
]
)
# execute both message polling and response handling loops
collected, handled = await gather(_collect_messages(ps), _collect_messages(stream_handler))
# No messages collected, since they're all responses
assert collected == [None, None]
assert collected == []
assert mock_process.call_count == 2
for msg in handled:
# responses have negative req
assert msg
assert msg.req == -2
@ -297,9 +346,8 @@ async def test_message_request(
assert json.loads(body.decode("utf-8")) == {"name": ["whoami"], "args": []}
assert ps.req_counter == 2
assert ps.register_handler.call_count == 1 # pylint: disable=no-member
assert ps.register_handler.call_count == 1 # type: ignore[attr-defined] # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mock_process = mocker.patch.object(handler, "process")
ps_server.feed(
@ -312,6 +360,8 @@ async def test_message_request(
assert mock_process.await_count == 1
# responses have negative req
assert msg
assert isinstance(msg.body, dict)
assert msg.req == -1
assert msg.body["id"] == "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"
assert ps.req_counter == 2

View File

@ -1,4 +1,26 @@
"""Test for utility functions"""
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2017 PySSB contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Tests for the utility functions"""
from base64 import b64decode
from unittest.mock import mock_open, patch