jsocket 1.9.3__py3-none-any.whl → 1.9.6__py3-none-any.whl
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.
- jsocket/__init__.py +72 -71
- jsocket/_version.py +3 -0
- jsocket/jsocket_base.py +214 -192
- jsocket/tserver.py +357 -189
- jsocket-1.9.6.dist-info/METADATA +166 -0
- jsocket-1.9.6.dist-info/RECORD +9 -0
- {jsocket-1.9.3.dist-info → jsocket-1.9.6.dist-info}/WHEEL +1 -1
- jsocket-1.9.6.dist-info/licenses/LICENSE +201 -0
- jsocket-1.9.3.dist-info/LICENSE +0 -21
- jsocket-1.9.3.dist-info/METADATA +0 -24
- jsocket-1.9.3.dist-info/RECORD +0 -8
- {jsocket-1.9.3.dist-info → jsocket-1.9.6.dist-info}/top_level.txt +0 -0
jsocket/__init__.py
CHANGED
|
@@ -1,75 +1,76 @@
|
|
|
1
1
|
""" @package jsocket
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
2
|
+
@brief Main package importing two modules, jsocket_base and tserver into the scope of jsocket.
|
|
3
|
+
|
|
4
|
+
@example example_servers.py
|
|
5
|
+
|
|
6
|
+
@mainpage JSocket - Fast & Scalable JSON Server & Client
|
|
7
|
+
@section Installation
|
|
8
|
+
|
|
9
|
+
The jsocket package should always be installed using the stable PyPi releases.
|
|
10
|
+
Either use "easy_install jsocket" or "pip install jsocket" to get the latest stable version.
|
|
11
|
+
|
|
12
|
+
@section Usage
|
|
13
|
+
|
|
14
|
+
The jsocket package is for use during the development of distributed systems. There are two ways to
|
|
15
|
+
use the package. The first and simplest is to create a custom single threaded server by overloading the
|
|
16
|
+
the jsocket.ThreadedServer class (see example one below).
|
|
17
|
+
|
|
18
|
+
The second, is to use the server factory functionality by overloading the jsocket.ServerFactoryThread
|
|
19
|
+
class and passing the declaration to the jsocket.ServerFactory(FactoryThread) object. This creates a
|
|
20
|
+
multithreaded custom JSON server for any number of simultaneous clients (see example two below).
|
|
21
|
+
|
|
22
|
+
@section Examples
|
|
23
|
+
@b 1: The following snippet simply creates a custom single threaded server by overloading jsocket.ThreadedServer
|
|
24
|
+
@code
|
|
25
|
+
class MyServer(jsocket.ThreadedServer):
|
|
26
|
+
# This is a basic example of a custom ThreadedServer.
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super(MyServer, self).__init__()
|
|
29
|
+
self.timeout = 2.0
|
|
30
|
+
logger.warning("MyServer class in customServer is for example purposes only.")
|
|
31
|
+
|
|
32
|
+
def _process_message(self, obj):
|
|
33
|
+
# virtual method
|
|
34
|
+
if obj != '':
|
|
35
|
+
if obj['message'] == "new connection":
|
|
36
|
+
logger.info("new connection.")
|
|
37
|
+
@endcode
|
|
38
|
+
|
|
39
|
+
@b 2: The following snippet creates a custom factory thread and starts a factory server. The factory server
|
|
40
|
+
will allocate and run a factory thread for each new client.
|
|
41
|
+
|
|
42
|
+
@code
|
|
43
|
+
import jsocket
|
|
44
|
+
|
|
45
|
+
class MyFactoryThread(jsocket.ServerFactoryThread):
|
|
46
|
+
# This is an example factory thread, which the server factory will
|
|
47
|
+
# instantiate for each new connection.
|
|
48
|
+
def __init__(self):
|
|
49
|
+
super(MyFactoryThread, self).__init__()
|
|
50
|
+
self.timeout = 2.0
|
|
51
|
+
|
|
52
|
+
def _process_message(self, obj):
|
|
53
|
+
# virtual method - Implementer must define protocol
|
|
54
|
+
if obj != '':
|
|
55
|
+
if obj['message'] == "new connection":
|
|
56
|
+
logger.info("new connection.")
|
|
57
|
+
else:
|
|
58
|
+
logger.info(obj)
|
|
59
|
+
|
|
60
|
+
server = jsocket.ServerFactory(MyFactoryThread)
|
|
61
|
+
server.timeout = 2.0
|
|
62
|
+
server.start()
|
|
63
|
+
|
|
64
|
+
client = jsocket.JsonClient()
|
|
65
|
+
client.connect()
|
|
66
|
+
client.send_obj({"message": "new connection"})
|
|
67
|
+
|
|
68
|
+
client.close()
|
|
69
|
+
server.stop()
|
|
70
|
+
server.join()
|
|
71
|
+
@endcode
|
|
72
|
+
|
|
73
73
|
"""
|
|
74
74
|
from jsocket.jsocket_base import *
|
|
75
75
|
from jsocket.tserver import *
|
|
76
|
+
from ._version import __version__
|
jsocket/_version.py
ADDED
jsocket/jsocket_base.py
CHANGED
|
@@ -1,208 +1,230 @@
|
|
|
1
1
|
""" @namespace jsocket_base
|
|
2
|
-
|
|
2
|
+
Contains JsonSocket, JsonServer and JsonClient implementations (json object message passing server and client).
|
|
3
3
|
"""
|
|
4
|
-
__author__
|
|
5
|
-
__email__
|
|
4
|
+
__author__ = "Christopher Piekarski"
|
|
5
|
+
__email__ = "chris@cpiekarski.com"
|
|
6
6
|
__copyright__= """
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Christopher Piekarski <chris@cpiekarski.com>
|
|
7
|
+
Copyright (C) 2011 by
|
|
8
|
+
Christopher Piekarski <chris@cpiekarski.com>
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
(at your option) any later version.
|
|
10
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
you may not use this file except in compliance with the License.
|
|
12
|
+
You may obtain a copy of the License at
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
-
GNU General Public License for more details.
|
|
20
|
-
|
|
21
|
-
You should have received a copy of the GNU General Public License
|
|
22
|
-
along with jsocket_base module. If not, see <http://www.gnu.org/licenses/>."""
|
|
23
|
-
__version__ = "1.0.2"
|
|
14
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
24
15
|
|
|
16
|
+
Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
See the License for the specific language governing permissions and
|
|
20
|
+
limitations under the License.
|
|
21
|
+
"""
|
|
25
22
|
import json
|
|
26
23
|
import socket
|
|
27
24
|
import struct
|
|
28
25
|
import logging
|
|
29
26
|
import time
|
|
30
27
|
|
|
28
|
+
from ._version import __version__
|
|
29
|
+
|
|
31
30
|
logger = logging.getLogger("jsocket")
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
32
|
+
|
|
33
|
+
def _socket_fileno(sock):
|
|
34
|
+
try:
|
|
35
|
+
return sock.fileno()
|
|
36
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JsonSocket:
|
|
41
|
+
"""Lightweight JSON-over-TCP socket wrapper with length-prefixed framing."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, address='127.0.0.1', port=5489, timeout=2.0):
|
|
44
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
45
|
+
self.conn = self.socket
|
|
46
|
+
self._timeout = timeout
|
|
47
|
+
self._address = address
|
|
48
|
+
self._port = port
|
|
49
|
+
self._last_client_addr = None
|
|
50
|
+
# Ensure the primary socket respects timeout for accept/connect operations
|
|
51
|
+
self.socket.settimeout(self._timeout)
|
|
52
|
+
|
|
53
|
+
def send_obj(self, obj):
|
|
54
|
+
"""Send a JSON-serializable object over the connection."""
|
|
55
|
+
msg = json.dumps(obj, ensure_ascii=False)
|
|
56
|
+
if self.socket:
|
|
57
|
+
payload = msg.encode('utf-8')
|
|
58
|
+
frmt = f"={len(payload)}s"
|
|
59
|
+
packed_msg = struct.pack(frmt, payload)
|
|
60
|
+
packed_hdr = struct.pack('!I', len(packed_msg))
|
|
61
|
+
self._send(packed_hdr)
|
|
62
|
+
self._send(packed_msg)
|
|
63
|
+
|
|
64
|
+
def _send(self, msg):
|
|
65
|
+
"""Send all bytes in `msg` to the peer."""
|
|
66
|
+
sent = 0
|
|
67
|
+
while sent < len(msg):
|
|
68
|
+
sent += self.conn.send(msg[sent:])
|
|
69
|
+
|
|
70
|
+
def _read(self, size):
|
|
71
|
+
"""Read exactly `size` bytes from the peer or raise on disconnect."""
|
|
72
|
+
data = b''
|
|
73
|
+
while len(data) < size:
|
|
74
|
+
data_tmp = self.conn.recv(size - len(data))
|
|
75
|
+
data += data_tmp
|
|
76
|
+
if data_tmp == b'':
|
|
77
|
+
raise RuntimeError("socket connection broken")
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
def _msg_length(self):
|
|
81
|
+
"""Read and unpack the 4-byte big-endian length header."""
|
|
82
|
+
d = self._read(4)
|
|
83
|
+
s = struct.unpack('!I', d)
|
|
84
|
+
return s[0]
|
|
85
|
+
|
|
86
|
+
def read_obj(self):
|
|
87
|
+
"""Read a full message and decode it as JSON, returning a Python object."""
|
|
88
|
+
size = self._msg_length()
|
|
89
|
+
data = self._read(size)
|
|
90
|
+
frmt = f"={size}s"
|
|
91
|
+
msg = struct.unpack(frmt, data)
|
|
92
|
+
return json.loads(msg[0].decode('utf-8'))
|
|
93
|
+
|
|
94
|
+
def close(self):
|
|
95
|
+
"""Close active connection and the listening socket if open."""
|
|
96
|
+
logger.debug(
|
|
97
|
+
"closing sockets (socket fd=%s, conn fd=%s)",
|
|
98
|
+
_socket_fileno(self.socket),
|
|
99
|
+
_socket_fileno(self.conn),
|
|
100
|
+
)
|
|
101
|
+
self._close_connection()
|
|
102
|
+
self._close_socket()
|
|
103
|
+
|
|
104
|
+
def _close_socket(self):
|
|
105
|
+
"""Best-effort shutdown and close of the main socket."""
|
|
106
|
+
logger.debug("closing main socket (fd=%s)", _socket_fileno(self.socket))
|
|
107
|
+
try:
|
|
108
|
+
if self.socket and self.socket.fileno() != -1:
|
|
109
|
+
try:
|
|
110
|
+
self.socket.shutdown(socket.SHUT_RDWR)
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
try:
|
|
114
|
+
self.socket.close()
|
|
115
|
+
except OSError:
|
|
116
|
+
pass
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def _close_connection(self):
|
|
121
|
+
"""Best-effort shutdown and close of the accepted connection socket."""
|
|
122
|
+
logger.debug("closing connection socket (fd=%s)", _socket_fileno(self.conn))
|
|
123
|
+
try:
|
|
124
|
+
if self.conn and self.conn is not self.socket and self.conn.fileno() != -1:
|
|
125
|
+
try:
|
|
126
|
+
self.conn.shutdown(socket.SHUT_RDWR)
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
try:
|
|
130
|
+
self.conn.close()
|
|
131
|
+
except OSError:
|
|
132
|
+
pass
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def _get_timeout(self):
|
|
137
|
+
"""Get the current socket timeout in seconds."""
|
|
138
|
+
return self._timeout
|
|
139
|
+
|
|
140
|
+
def _set_timeout(self, timeout):
|
|
141
|
+
"""Set the socket timeout in seconds and apply to the main socket."""
|
|
142
|
+
self._timeout = timeout
|
|
143
|
+
self.socket.settimeout(timeout)
|
|
144
|
+
|
|
145
|
+
def _get_address(self):
|
|
146
|
+
"""Return the configured bind address."""
|
|
147
|
+
return self._address
|
|
148
|
+
|
|
149
|
+
def _set_address(self, _address):
|
|
150
|
+
"""No-op: address is read-only after initialization."""
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _get_port(self):
|
|
154
|
+
"""Return the configured bind port."""
|
|
155
|
+
return self._port
|
|
156
|
+
|
|
157
|
+
def _set_port(self, _port):
|
|
158
|
+
"""No-op: port is read-only after initialization."""
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
timeout = property(_get_timeout, _set_timeout, doc='Get/set the socket timeout')
|
|
162
|
+
address = property(_get_address, _set_address, doc='read only property socket address')
|
|
163
|
+
port = property(_get_port, _set_port, doc='read only property socket port')
|
|
164
|
+
|
|
165
|
+
|
|
117
166
|
class JsonServer(JsonSocket):
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
"""Server socket that accepts one connection at a time."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, address='127.0.0.1', port=5489):
|
|
170
|
+
super().__init__(address, port)
|
|
171
|
+
self._bind()
|
|
172
|
+
|
|
173
|
+
def _bind(self):
|
|
174
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
175
|
+
self.socket.bind((self.address, self.port))
|
|
176
|
+
|
|
177
|
+
def _listen(self):
|
|
178
|
+
self.socket.listen(5)
|
|
179
|
+
|
|
180
|
+
def _accept(self):
|
|
181
|
+
return self.socket.accept()
|
|
182
|
+
|
|
183
|
+
def accept_connection(self):
|
|
184
|
+
"""Listen and accept a single client connection; set timeout accordingly."""
|
|
185
|
+
self._listen()
|
|
186
|
+
self.conn, addr = self._accept()
|
|
187
|
+
self._last_client_addr = addr
|
|
188
|
+
self.conn.settimeout(self.timeout)
|
|
189
|
+
logger.debug(
|
|
190
|
+
"connection accepted, conn socket (%s,%d,%s)", addr[0], addr[1], str(self.conn.gettimeout())
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _reset_connection_ref(self):
|
|
194
|
+
"""Reset the server's connection reference to the listening socket."""
|
|
195
|
+
self.conn = self.socket
|
|
196
|
+
|
|
197
|
+
def _is_connected(self):
|
|
198
|
+
try:
|
|
199
|
+
return (self.conn is not None) and (self.conn is not self.socket) and (self.conn.fileno() != -1)
|
|
200
|
+
except (OSError, AttributeError):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
connected = property(_is_connected, doc="True if server has an active client connection")
|
|
204
|
+
|
|
205
|
+
|
|
143
206
|
class JsonClient(JsonSocket):
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
logger.debug("starting JsonServer")
|
|
169
|
-
server = JsonServer()
|
|
170
|
-
server.accept_connection()
|
|
171
|
-
while 1:
|
|
172
|
-
try:
|
|
173
|
-
msg = server.read_obj()
|
|
174
|
-
logger.info("server received: %s" % msg)
|
|
175
|
-
server.send_obj(msg)
|
|
176
|
-
except socket.timeout as e:
|
|
177
|
-
logger.debug("server socket.timeout: %s" % e)
|
|
178
|
-
continue
|
|
179
|
-
except Exception as e:
|
|
180
|
-
logger.error("server: %s" % e)
|
|
181
|
-
break
|
|
182
|
-
|
|
183
|
-
server.close()
|
|
184
|
-
|
|
185
|
-
t = threading.Timer(1,server_thread)
|
|
186
|
-
t.start()
|
|
187
|
-
|
|
188
|
-
time.sleep(2)
|
|
189
|
-
logger.debug("starting JsonClient")
|
|
190
|
-
|
|
191
|
-
client = JsonClient()
|
|
192
|
-
client.connect()
|
|
193
|
-
|
|
194
|
-
i = 0
|
|
195
|
-
while i < 10:
|
|
196
|
-
client.send_obj({"i": i})
|
|
197
|
-
try:
|
|
198
|
-
msg = client.read_obj()
|
|
199
|
-
logger.info("client received: %s" % msg)
|
|
200
|
-
except socket.timeout as e:
|
|
201
|
-
logger.debug("client socket.timeout: %s" % e)
|
|
202
|
-
continue
|
|
203
|
-
except Exception as e:
|
|
204
|
-
logger.error("client: %s" % e)
|
|
205
|
-
break
|
|
206
|
-
i = i + 1
|
|
207
|
-
|
|
208
|
-
client.close()
|
|
207
|
+
"""Client socket for connecting to a JsonServer and exchanging JSON messages."""
|
|
208
|
+
|
|
209
|
+
def __init__(self, address='127.0.0.1', port=5489):
|
|
210
|
+
super().__init__(address, port)
|
|
211
|
+
|
|
212
|
+
def connect(self):
|
|
213
|
+
"""Attempt to connect to the server up to 10 times with backoff."""
|
|
214
|
+
for attempt in range(1, 11):
|
|
215
|
+
try:
|
|
216
|
+
logger.debug("connect attempt %d to %s:%s", attempt, self.address, self.port)
|
|
217
|
+
self.socket.connect((self.address, self.port))
|
|
218
|
+
except socket.error as msg:
|
|
219
|
+
logger.error("SockThread Error: %s", msg)
|
|
220
|
+
# Recreate the socket to avoid retrying on a potentially bad fd.
|
|
221
|
+
self._close_socket()
|
|
222
|
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
223
|
+
self.socket.settimeout(self._timeout)
|
|
224
|
+
self.conn = self.socket
|
|
225
|
+
logger.debug("recreated socket for retry %d to %s:%s", attempt, self.address, self.port)
|
|
226
|
+
time.sleep(3)
|
|
227
|
+
continue
|
|
228
|
+
logger.info("...Socket Connected")
|
|
229
|
+
return True
|
|
230
|
+
return False
|