picows 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: picows
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Fast websocket client and server for asyncio
5
5
  Author-email: Taras Kozlov <tarasko.projects@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/tarasko/picows
@@ -28,6 +28,18 @@ Description-Content-Type: text/x-rst
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest; extra == "dev"
30
30
 
31
+ .. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
32
+ :target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
33
+ :alt: GitHub Actions status for master branch
34
+
35
+ .. image:: https://badge.fury.io/py/picows.svg
36
+ :target: https://pypi.org/project/picows
37
+ :alt: Latest PyPI package version
38
+
39
+ .. image:: https://img.shields.io/pypi/dm/picows
40
+ :target: https://pypistats.org/packages/picows
41
+ :alt: Downloads count
42
+
31
43
  picows is a library for building WebSocket clients and servers with a focus on performance.
32
44
 
33
45
  Performance
@@ -57,9 +69,11 @@ These features come with a significant cost even when messages are small, unfrag
57
69
 
58
70
  API Design
59
71
  ----------
60
- The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
61
- It passes frames instead of messages to a user handler. A message can potentially consist of multiple frame but it is up to user to choose the best strategy for merging frames.
62
- Though the most common case is when messages and frames are the same i.e. a message consists of only a single frame.
72
+ The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
73
+ It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
74
+ Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
75
+
76
+ That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
63
77
 
64
78
  Getting started
65
79
  ---------------
@@ -76,6 +90,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
76
90
 
77
91
  class ClientListener(WSListener):
78
92
  def on_ws_connected(self, transport: WSTransport):
93
+ self.transport = transport
79
94
  transport.send(WSMsgType.TEXT, b"Hello world")
80
95
 
81
96
  def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
@@ -84,9 +99,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
84
99
 
85
100
 
86
101
  async def main(endpoint):
87
- # ClientListener instance will be created after successfull accept and http upgrade.
88
102
  (_, client) = await ws_connect(endpoint, ClientListener, "client")
89
- await client._transport.wait_until_closed()
103
+ await client.transport.wait_until_closed()
90
104
 
91
105
 
92
106
  if __name__ == '__main__':
@@ -106,7 +120,7 @@ Echo server
106
120
 
107
121
  import asyncio
108
122
  import uvloop
109
- from picows import WSFrame, WSTransport, WSListener, ws_connect, WSMsgType
123
+ from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
110
124
 
111
125
  class ServerClientListener(WSListener):
112
126
  def on_ws_connected(self, transport: WSTransport):
@@ -119,7 +133,6 @@ Echo server
119
133
 
120
134
  async def main():
121
135
  url = "ws://127.0.0.1:9001"
122
- # ServerClientListener instance will be created for each client after accept and successfull http upgrade.
123
136
  server = await ws_create_server(url, ServerClientListener, "server")
124
137
  print(f"Server started on {url}")
125
138
  await server.serve_forever()
@@ -1,3 +1,15 @@
1
+ .. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
2
+ :target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
3
+ :alt: GitHub Actions status for master branch
4
+
5
+ .. image:: https://badge.fury.io/py/picows.svg
6
+ :target: https://pypi.org/project/picows
7
+ :alt: Latest PyPI package version
8
+
9
+ .. image:: https://img.shields.io/pypi/dm/picows
10
+ :target: https://pypistats.org/packages/picows
11
+ :alt: Downloads count
12
+
1
13
  picows is a library for building WebSocket clients and servers with a focus on performance.
2
14
 
3
15
  Performance
@@ -27,9 +39,11 @@ These features come with a significant cost even when messages are small, unfrag
27
39
 
28
40
  API Design
29
41
  ----------
30
- The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
31
- It passes frames instead of messages to a user handler. A message can potentially consist of multiple frame but it is up to user to choose the best strategy for merging frames.
32
- Though the most common case is when messages and frames are the same i.e. a message consists of only a single frame.
42
+ The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
43
+ It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
44
+ Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
45
+
46
+ That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
33
47
 
34
48
  Getting started
35
49
  ---------------
@@ -46,6 +60,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
46
60
 
47
61
  class ClientListener(WSListener):
48
62
  def on_ws_connected(self, transport: WSTransport):
63
+ self.transport = transport
49
64
  transport.send(WSMsgType.TEXT, b"Hello world")
50
65
 
51
66
  def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
@@ -54,9 +69,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
54
69
 
55
70
 
56
71
  async def main(endpoint):
57
- # ClientListener instance will be created after successfull accept and http upgrade.
58
72
  (_, client) = await ws_connect(endpoint, ClientListener, "client")
59
- await client._transport.wait_until_closed()
73
+ await client.transport.wait_until_closed()
60
74
 
61
75
 
62
76
  if __name__ == '__main__':
@@ -76,7 +90,7 @@ Echo server
76
90
 
77
91
  import asyncio
78
92
  import uvloop
79
- from picows import WSFrame, WSTransport, WSListener, ws_connect, WSMsgType
93
+ from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
80
94
 
81
95
  class ServerClientListener(WSListener):
82
96
  def on_ws_connected(self, transport: WSTransport):
@@ -89,7 +103,6 @@ Echo server
89
103
 
90
104
  async def main():
91
105
  url = "ws://127.0.0.1:9001"
92
- # ServerClientListener instance will be created for each client after accept and successfull http upgrade.
93
106
  server = await ws_create_server(url, ServerClientListener, "server")
94
107
  print(f"Server started on {url}")
95
108
  await server.serve_forever()
@@ -0,0 +1,23 @@
1
+ __version__ = "0.2.2"
2
+
3
+ from .picows import (
4
+ WSMsgType,
5
+ WSCloseCode,
6
+ WSFrame,
7
+ WSTransport,
8
+ WSListener,
9
+ ws_connect,
10
+ ws_create_server,
11
+ PICOWS_DEBUG_LL
12
+ )
13
+
14
+ __all__ = [
15
+ 'WSMsgType',
16
+ 'WSCloseCode',
17
+ 'WSFrame',
18
+ 'WSTransport',
19
+ 'WSListener',
20
+ 'ws_connect',
21
+ 'ws_create_server',
22
+ 'PICOWS_DEBUG_LL'
23
+ ]
@@ -59,8 +59,8 @@ cdef class WSFrame:
59
59
  readonly uint8_t fin
60
60
  readonly uint8_t last_in_buffer
61
61
 
62
- # Creates a new python object every time
63
62
  cpdef bytes get_payload_as_bytes(self)
63
+ cpdef str get_payload_as_utf8_text(self)
64
64
  cpdef str get_payload_as_ascii_text(self)
65
65
  cpdef object get_payload_as_memoryview(self)
66
66
 
@@ -23,21 +23,55 @@ from libc.stdlib cimport rand
23
23
  PICOWS_DEBUG_LL = 9
24
24
 
25
25
 
26
- cdef extern from "arpa/inet.h" nogil:
26
+ cdef extern from * nogil:
27
+ """
28
+ #if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__)
29
+ # define __WINDOWS__
30
+ #endif
31
+
32
+ #if defined(__linux__)
33
+ #include <arpa/inet.h>
34
+ #include <endian.h>
35
+ #elif defined(__APPLE__)
36
+ #include <arpa/inet.h>
37
+ #include <libkern/OSByteOrder.h>
38
+ #define be64toh(x) OSSwapBigToHostInt64(x)
39
+ #define htobe64(x) OSSwapHostToBigInt64(x)
40
+ #elif defined(__OpenBSD__)
41
+ #include <arpa/inet.h>
42
+ #include <sys/endian.h>
43
+ #elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__)
44
+ #include <arpa/inet.h>
45
+ #include <sys/endian.h>
46
+ #define be64toh(x) betoh64(x)
47
+ #elif defined(__WINDOWS__)
48
+ #include <winsock2.h>
49
+ #if BYTE_ORDER == LITTLE_ENDIAN
50
+ #define be64toh(x) ntohll(x)
51
+ #define htobe64(x) htonll(x)
52
+ #elif BYTE_ORDER == BIG_ENDIAN
53
+ #define be64toh(x) (x)
54
+ #define htobe64(x) (x)
55
+ #endif
56
+ #else
57
+ error byte order not supported
58
+ #endif
59
+ """
60
+
61
+ # Network order is big-endian
62
+
27
63
  uint32_t ntohl(uint32_t)
28
64
  uint32_t htonl(uint32_t)
29
65
  uint16_t ntohs(uint16_t)
30
66
  uint16_t htons(uint16_t)
31
67
 
32
-
33
- cdef extern from "endian.h" nogil:
34
- # Network order is big-endian
35
68
  uint64_t be64toh(uint64_t)
36
69
  uint64_t htobe64(uint64_t)
37
70
 
38
71
 
39
72
  cdef extern from "Python.h":
40
73
  PyObject *PyUnicode_FromStringAndSize(const char *u, Py_ssize_t size)
74
+ PyObject *PyUnicode_DecodeASCII(char *s, Py_ssize_t size, char *errors)
41
75
 
42
76
 
43
77
  class PicowsError(Exception):
@@ -79,15 +113,40 @@ cdef _mask_payload(uint8_t* input, size_t input_len, uint32_t mask):
79
113
  @cython.freelist(64)
80
114
  cdef class WSFrame:
81
115
  cpdef bytes get_payload_as_bytes(self):
116
+ """
117
+ Returns a new bytes object with a copy of frame payload.
118
+ """
82
119
  return PyBytes_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
83
120
 
121
+ cpdef str get_payload_as_utf8_text(self):
122
+ """
123
+ Interpret payload as UTF8 text and returns a new str object.
124
+ Behaviour is underfined (most likely python will crash) if payload doesn't contain a valid UTF8
125
+ """
126
+ cdef str s = <str>PyUnicode_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
127
+ # Workaround for broken cython reference counting
128
+ Py_DECREF(s)
129
+ return s
130
+
84
131
  cpdef str get_payload_as_ascii_text(self):
132
+ """
133
+ Interpret payload as UTF8 text and returns a new str object.
134
+ Behaviour is underfined (most likely python will crash) if payload doesn't contain a valid UTF8
135
+ """
136
+ cdef PyObject* ptr = PyUnicode_DecodeASCII(self.payload_ptr, <Py_ssize_t>self.payload_size, NULL)
137
+ if ptr == NULL:
138
+ raise PicowsError("payload doesn't contain ASCII string")
139
+ cdef str s = <str>ptr
85
140
  # Workaround for broken cython reference counting
86
- cdef str s = <str> PyUnicode_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
87
141
  Py_DECREF(s)
88
142
  return s
89
143
 
90
144
  cpdef object get_payload_as_memoryview(self):
145
+ """
146
+ Return continous memoryview to a parser buffer with payload.
147
+ Memoryview content will be invalidated after on_ws_frame is complete.
148
+ Please process payload or copy it as soon as possible.
149
+ """
91
150
  return PyMemoryView_FromMemory(self.payload_ptr, <Py_ssize_t>self.payload_size, PyBUF_READ)
92
151
 
93
152
  cpdef WSCloseCode get_close_code(self):
@@ -264,7 +323,7 @@ cdef class WSFrameParser:
264
323
  if rsv1 or rsv2 or rsv3:
265
324
  mem_dump = PyBytes_FromStringAndSize(
266
325
  self._buffer.data + self._f_curr_state_start_pos,
267
- max(self._f_new_data_start_pos - self._f_curr_state_start_pos, 64)
326
+ max(self._f_new_data_start_pos - self._f_curr_state_start_pos, <size_t>64)
268
327
  )
269
328
  raise PicowsError(
270
329
  WSCloseCode.PROTOCOL_ERROR,
@@ -627,7 +686,6 @@ cdef class WSProtocol:
627
686
  WSFrameParser _frame_parser
628
687
  object _loop
629
688
  object _handshake_timeout_handle
630
- object _listener_factory
631
689
  bint _is_client_side
632
690
  bint _log_debug_enabled
633
691
 
@@ -641,12 +699,11 @@ cdef class WSProtocol:
641
699
  self._frame_parser = None
642
700
  self._loop = asyncio.get_running_loop()
643
701
  self._handshake_timeout_handle = None
644
- self._listener_factory = ws_listener_factory
645
702
  self._is_client_side = is_client_side
646
703
  self._log_debug_enabled = self._logger.isEnabledFor(PICOWS_DEBUG_LL)
647
704
 
648
705
  self.transport = None
649
- self.listener = None
706
+ self.listener = ws_listener_factory()
650
707
 
651
708
  def connection_made(self, transport: asyncio.Transport):
652
709
  sock = transport.get_extra_info('socket')
@@ -654,24 +711,27 @@ cdef class WSProtocol:
654
711
  sockname = transport.get_extra_info('sockname')
655
712
 
656
713
  sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
657
- sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
714
+ if hasattr(socket, "TCP_QUICKACK"):
715
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
658
716
 
659
717
  self._logger = self._logger.getChild(str(sock.fileno()))
660
718
  self._frame_parser = WSFrameParser(self._logger)
661
719
 
720
+ quickack = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK) if hasattr(socket, "TCP_QUICKACK") else False
721
+
662
722
  if self._is_client_side:
663
723
  self._logger.info("WS connection established: %s -> %s, recvbuf=%d, sendbuf=%d, quickack=%d, nodelay=%d",
664
724
  peername, sockname,
665
725
  sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF),
666
726
  sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF),
667
- sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK),
727
+ quickack,
668
728
  sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
669
729
  else:
670
730
  self._logger.info("New connection accepted: %s -> %s, recvbuf=%d, sendbuf=%d, quickack=%d, nodelay=%d",
671
731
  peername, sockname,
672
732
  sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF),
673
733
  sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF),
674
- sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK),
734
+ quickack,
675
735
  sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
676
736
 
677
737
 
@@ -717,8 +777,6 @@ cdef class WSProtocol:
717
777
  self._frame_parser.handshake_complete_future.set_result(None)
718
778
  self._handshake_timeout_handle.cancel()
719
779
  self._handshake_timeout_handle = None
720
- self.listener = self._listener_factory()
721
- self._listener_factory = None
722
780
  self.listener.on_ws_connected(self.transport)
723
781
 
724
782
  cdef WSFrame frame = self._get_next_frame()
@@ -793,7 +851,7 @@ async def ws_connect(str url, ws_listener_factory, str logger_name, ssl_context=
793
851
  else:
794
852
  raise ValueError(f"invalid url scheme: {url}")
795
853
 
796
- ws_protocol_factory = lambda: WSProtocol(url_parts.netloc, url_parts.path, True, None, logger_name)
854
+ ws_protocol_factory = lambda: WSProtocol(url_parts.netloc, url_parts.path, True, ws_listener_factory, logger_name)
797
855
 
798
856
  cdef WSProtocol ws_protocol
799
857
 
@@ -802,7 +860,6 @@ async def ws_connect(str url, ws_listener_factory, str logger_name, ssl_context=
802
860
  ssl_handshake_timeout=ssl_handshake_timeout, ssl_shutdown_timeout=ssl_shutdown_timeout)
803
861
 
804
862
  await ws_protocol.wait_until_handshake_complete()
805
- ws_protocol.listener = ws_listener_factory()
806
863
  ws_protocol.listener.on_ws_connected(ws_protocol.transport)
807
864
 
808
865
  return ws_protocol.transport, ws_protocol.listener
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: picows
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Fast websocket client and server for asyncio
5
5
  Author-email: Taras Kozlov <tarasko.projects@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/tarasko/picows
@@ -28,6 +28,18 @@ Description-Content-Type: text/x-rst
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest; extra == "dev"
30
30
 
31
+ .. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
32
+ :target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
33
+ :alt: GitHub Actions status for master branch
34
+
35
+ .. image:: https://badge.fury.io/py/picows.svg
36
+ :target: https://pypi.org/project/picows
37
+ :alt: Latest PyPI package version
38
+
39
+ .. image:: https://img.shields.io/pypi/dm/picows
40
+ :target: https://pypistats.org/packages/picows
41
+ :alt: Downloads count
42
+
31
43
  picows is a library for building WebSocket clients and servers with a focus on performance.
32
44
 
33
45
  Performance
@@ -57,9 +69,11 @@ These features come with a significant cost even when messages are small, unfrag
57
69
 
58
70
  API Design
59
71
  ----------
60
- The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
61
- It passes frames instead of messages to a user handler. A message can potentially consist of multiple frame but it is up to user to choose the best strategy for merging frames.
62
- Though the most common case is when messages and frames are the same i.e. a message consists of only a single frame.
72
+ The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
73
+ It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
74
+ Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
75
+
76
+ That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
63
77
 
64
78
  Getting started
65
79
  ---------------
@@ -76,6 +90,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
76
90
 
77
91
  class ClientListener(WSListener):
78
92
  def on_ws_connected(self, transport: WSTransport):
93
+ self.transport = transport
79
94
  transport.send(WSMsgType.TEXT, b"Hello world")
80
95
 
81
96
  def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
@@ -84,9 +99,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
84
99
 
85
100
 
86
101
  async def main(endpoint):
87
- # ClientListener instance will be created after successfull accept and http upgrade.
88
102
  (_, client) = await ws_connect(endpoint, ClientListener, "client")
89
- await client._transport.wait_until_closed()
103
+ await client.transport.wait_until_closed()
90
104
 
91
105
 
92
106
  if __name__ == '__main__':
@@ -106,7 +120,7 @@ Echo server
106
120
 
107
121
  import asyncio
108
122
  import uvloop
109
- from picows import WSFrame, WSTransport, WSListener, ws_connect, WSMsgType
123
+ from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
110
124
 
111
125
  class ServerClientListener(WSListener):
112
126
  def on_ws_connected(self, transport: WSTransport):
@@ -119,7 +133,6 @@ Echo server
119
133
 
120
134
  async def main():
121
135
  url = "ws://127.0.0.1:9001"
122
- # ServerClientListener instance will be created for each client after accept and successfull http upgrade.
123
136
  server = await ws_create_server(url, ServerClientListener, "server")
124
137
  print(f"Server started on {url}")
125
138
  await server.serve_forever()
@@ -9,4 +9,5 @@ picows.egg-info/PKG-INFO
9
9
  picows.egg-info/SOURCES.txt
10
10
  picows.egg-info/dependency_links.txt
11
11
  picows.egg-info/requires.txt
12
- picows.egg-info/top_level.txt
12
+ picows.egg-info/top_level.txt
13
+ tests/test_echo.py
@@ -7,8 +7,13 @@ vi = sys.version_info
7
7
  if vi < (3, 8):
8
8
  raise RuntimeError('picows requires Python 3.8 or greater')
9
9
 
10
+ if os.name == 'nt':
11
+ libraries = ["Ws2_32"]
12
+ else:
13
+ libraries = None
14
+
10
15
  cython_modules = [
11
- Extension("picows.picows", ["picows/picows.pyx"])
16
+ Extension("picows.picows", ["picows/picows.pyx"], libraries=libraries)
12
17
  ]
13
18
 
14
19
  if os.getenv("PICOWS_BUILD_EXAMPLES") is not None:
@@ -17,8 +22,8 @@ if os.getenv("PICOWS_BUILD_EXAMPLES") is not None:
17
22
  setup(
18
23
  ext_modules=cythonize(
19
24
  cython_modules,
20
- compiler_directives = {
21
- 'language_level' : sys.version_info[0],
25
+ compiler_directives={
26
+ 'language_level': sys.version_info[0],
22
27
  'profile': False,
23
28
  'nonecheck': False,
24
29
  'boundscheck': False,
@@ -31,4 +36,3 @@ setup(
31
36
  gdb_debug=False
32
37
  )
33
38
  )
34
-
@@ -0,0 +1,76 @@
1
+ import asyncio
2
+ import os
3
+
4
+ import pytest_asyncio
5
+
6
+ import picows
7
+ import pytest
8
+
9
+ URL = "ws://127.0.0.1:9001"
10
+
11
+
12
+ class WSFrameMaterialized:
13
+ def __init__(self, frame: picows.WSFrame):
14
+ self.opcode = frame.opcode
15
+ self.payload_as_bytes = frame.get_payload_as_bytes()
16
+ self.payload_as_bytes_from_mv = bytes(frame.get_payload_as_memoryview())
17
+ self.fin = frame.fin
18
+
19
+
20
+ #@pytest.fixture(scope="module")
21
+ @pytest.fixture
22
+ async def echo_server():
23
+ class PicowsServerListener(picows.WSListener):
24
+ def on_ws_connected(self, transport: picows.WSTransport):
25
+ print("echo_server:on_ws_connected")
26
+ self._transport = transport
27
+
28
+ def on_ws_frame(self, transport: picows.WSTransport, frame: picows.WSFrame):
29
+ print("echo_server:on_ws_frame")
30
+ self._transport.send(frame.opcode, frame.get_payload_as_bytes())
31
+ if frame.opcode == picows.WSMsgType.CLOSE:
32
+ self._transport.disconnect()
33
+
34
+ server = await picows.ws_create_server(URL, PicowsServerListener, "server")
35
+ task = asyncio.create_task(server.serve_forever())
36
+ print("initiated module level echo server")
37
+ yield server
38
+
39
+ # Teardown server
40
+ task.cancel()
41
+ try:
42
+ await(task)
43
+ except:
44
+ pass
45
+
46
+ print("stopped module level echo server")
47
+
48
+
49
+ # @pytest.fixture(scope="module")
50
+ @pytest.fixture
51
+ async def echo_client(echo_server):
52
+ class PicowsClientListener(picows.WSListener):
53
+ def on_ws_connected(self, transport: picows.WSTransport):
54
+ self.transport = transport
55
+ self.msg_queue = asyncio.Queue()
56
+
57
+ def on_ws_frame(self, transport: picows.WSTransport, frame: picows.WSFrame):
58
+ self.msg_queue.put_nowait(WSFrameMaterialized(frame))
59
+
60
+ (_, client) = await picows.ws_connect(URL, PicowsClientListener, "client")
61
+ yield client
62
+
63
+ # Teardown client
64
+ client.transport.disconnect()
65
+ await client.transport.wait_until_closed()
66
+
67
+
68
+ @pytest.mark.parametrize("msg_size", [32, 1024, 20000])
69
+ async def test_echo(echo_client, msg_size):
70
+ msg = os.urandom(msg_size)
71
+ echo_client.transport.send(picows.WSMsgType.BINARY, msg)
72
+ frame: WSFrameMaterialized = await echo_client.msg_queue.get()
73
+ assert frame.opcode == picows.WSMsgType.BINARY
74
+ assert frame.payload_as_bytes == msg
75
+ # assert frame.payload_as_ascii_text == msg.decode("ascii")
76
+ assert frame.payload_as_bytes_from_mv == msg
@@ -1,12 +0,0 @@
1
- __version__ = "0.2.0"
2
-
3
- from .picows import (
4
- WSMsgType,
5
- WSCloseCode,
6
- WSFrame,
7
- WSTransport,
8
- WSListener,
9
- ws_connect,
10
- ws_create_server,
11
- PICOWS_DEBUG_LL
12
- )
File without changes
File without changes
File without changes