44# --------------------------------------------------------------------------------------------
55
66from io import BytesIO
7+ from enum import Enum
78import json
89
9- class JSON_RPC_Writer (object ):
10+ class Read_State (Enum ):
11+ Header = 1
12+ Content = 2
13+
14+ class Json_Rpc_Writer (object ):
15+ """
16+ Writes to the supplied stream through the JSON RPC Protocol where a request is formatted through a method
17+ name and the necessary parameters.
18+ """
1019 HEADER = "Content-Length: {0}\r \n \r \n "
1120
1221 def __init__ (self , stream , encoding = None ):
@@ -15,7 +24,10 @@ def __init__(self, stream, encoding = None):
1524 if encoding is None :
1625 self .encoding = 'UTF-8'
1726
18- def send_request (self , method , params , id ):
27+ def send_request (self , method , params , id = None ):
28+ """
29+ Forms and writes a JSON RPC protocol compliant request a method and it's parameters to the stream.
30+ """
1931 # Perhaps move to a different def to add some validation
2032 content_body = {
2133 "jsonrpc" : "2.0" ,
@@ -27,10 +39,16 @@ def send_request(self, method, params, id):
2739 json_content = json .dumps (content_body )
2840 header = self .HEADER .format (str (len (json_content )))
2941
30- self .stream .write (header .encode (" ascii" ))
42+ self .stream .write (header .encode (' ascii' ))
3143 self .stream .write (json_content .encode (self .encoding ))
32-
33- class JSON_RPC_Reader (object ):
44+ self .stream .flush ()
45+
46+ class Json_Rpc_Reader (object ):
47+ """
48+ Reads from the supplied stream through the JSON RPC Protocol. A Content-length header is required in the format
49+ of "Content-Length: <number of bytes>".
50+ Various exceptions may occur during the read process and are documented in each method.
51+ """
3452 # \r\n
3553 CR = 13
3654 LF = 10
@@ -50,31 +68,46 @@ def __init__(self, stream, encoding = None):
5068 self .read_offset = 0
5169 self .expected_content_length = 0
5270 self .headers = {}
53- #TODO: Create enum
54- self .read_state = "Header"
71+ self .read_state = Read_State .Header
5572
5673 def read_response (self ):
74+ """
75+ Reads the response from the supplied stream by chunks into a buffer until all headers and body content are read.
76+
77+ Returns the response body content in JSON
78+ Exceptions raised:
79+ ValueError
80+ if the body-content can not be serialized to a JSON object
81+ """
5782 # Using a mutable list to hold the value since a immutable string passed by reference won't change the value
5883 content = ["" ]
5984 while (self .read_next_chunk ()):
6085 # If we can't read a header, read the next chunk
61- if (self .read_state == " Header" and not self .try_read_headers ()):
86+ if (self .read_state is Read_State . Header and not self .try_read_headers ()):
6287 continue
6388 # If we read the header, try the content. If that fails, read the next chunk
64- if (self .read_state == " Content" and not self .try_read_content (content )):
89+ if (self .read_state is Read_State . Content and not self .try_read_content (content )):
6590 continue
6691 # We have the content
6792 break
6893
6994 # Resize buffer and remove bytes we have read
70- self .shift_buffer_bytes_and_reset (self .read_offset )
95+ self .trim_buffer_and_resize (self .read_offset )
7196 try :
7297 return json .loads (content [0 ])
73- except ValueError as error :
74- # response has invalid json object, throw Exception TODO: log message
98+ except ValueError :
99+ # response has invalid json object, throw Exception TODO: log message to telemetry
75100 raise
76101
77102 def read_next_chunk (self ):
103+ """
104+ Reads a chunk of the stream into the byte array. Buffer size is doubled if less than 25% of buffer space is available.abs
105+ Exceptions raised:
106+ EOFError
107+ Stream was empty or Stream did not contain a valid header or content-body
108+ IOError
109+ Stream was closed externally
110+ """
78111 # Check if we need to resize
79112 current_buffer_size = len (self .buffer )
80113 if ((current_buffer_size - float (self .buffer_end_offset )) / current_buffer_size ) < self .BUFFER_RESIZE_TRIGGER :
@@ -92,17 +125,23 @@ def read_next_chunk(self):
92125
93126 if (length_read == 0 ):
94127 # Nothing was read, could be due to the server process shutting down while leaving stream open
95- # close stream and return false and/or throw exception?
96- # for now throwing exception
97128 raise EOFError ("End of stream reached with no valid header or content-body" )
98129
99130 return True
100131
101- except Exception :
102- #TODO: Add more granular exception message
132+ except IOError as error :
133+ # TODO: add to telemetry
103134 raise
104135
105136 def try_read_headers (self ):
137+ """
138+ Attempts to read the Header information from the internal buffer expending the last header contain "\r \n \r \n .
139+
140+ Returns false if the header was not found.
141+ Exceptions:
142+ LookupError The content-length header was not found
143+ ValueError The content-length contained a invalid literal for int
144+ """
106145 # Scan the buffer up until right before the CRLFCRLF
107146 scan_offset = self .read_offset
108147 while (scan_offset + 3 < self .buffer_end_offset and
@@ -123,42 +162,51 @@ def try_read_headers(self):
123162 colon_index = header .find (':' )
124163
125164 if (colon_index == - 1 ):
126- raise KeyError ("Colon missing from Header" )
165+ raise KeyError ("Colon missing from Header: {0}." . format ( header ) )
127166
128- header_key = header [:colon_index ]
167+ # Making all headers lowercase to support case insensitivity
168+ header_key = header [:colon_index ].lower ()
129169 header_value = header [colon_index + 1 :]
130170
131171 self .headers [header_key ] = header_value
132172
133173 #Find content body in the list of headers and parse the Value
134- if (self . headers [ "Content-Length" ] is None ):
135- raise LookupError ("Content Length was not found in headers received" )
174+ if (not ( "content-length" in self . headers ) ):
175+ raise LookupError ("Content- Length was not found in headers received. " )
136176
137- self .expected_content_length = int (self .headers ["Content-Length " ])
138-
139- except Exception :
140- # Trash the buffer we read and shift past read content
141- self .shift_buffer_bytes_and_reset ( self . scan_offset + 4 )
177+ self .expected_content_length = int (self .headers ["content-length " ])
178+
179+ except ValueError :
180+ # Content-length contained invalid literal for int
181+ self .trim_buffer_and_resize ( scan_offset + 4 )
142182 raise
143183
144184 # Pushing read pointer past the newline characters
145185 self .read_offset = scan_offset + 4
146- # TODO: Create enum for this
147- self . read_state = "Content"
186+ self . read_state = Read_State . Content
187+
148188 return True
149189
150190 def try_read_content (self , content ):
191+ """
192+ Attempts to read the content from the internal buffer.
193+
194+ Returns false if buffer does not contain the entire content.
195+ """
151196 # if we buffered less than the expected content length, return false
152197 if (self .buffer_end_offset - self .read_offset < self .expected_content_length ):
153198 return False
154199
155200 content [0 ] = self .buffer [self .read_offset :self .read_offset + self .expected_content_length ].decode (self .encoding )
156201 self .read_offset += self .expected_content_length
157- #TODO: Create a enum for this
158- self .read_state = " Header"
202+
203+ self .read_state = Read_State . Header
159204 return True
160205
161- def shift_buffer_bytes_and_reset (self , bytes_to_remove ):
206+ def trim_buffer_and_resize (self , bytes_to_remove ):
207+ """
208+ Trims the buffer by the passed in bytes_to_remove by creating a new buffer that is at a minimum the default max size.
209+ """
162210 current_buffer_size = len (self .buffer )
163211 # Create a new buffer with either minumum size or leftover size
164212 new_buffer = bytearray (max (current_buffer_size - bytes_to_remove , self .DEFAULT_BUFFER_SIZE ))
0 commit comments