vortexpy 4.0.23__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.
- vortex/DataWrapTuple.py +27 -0
- vortex/DeferUtil.py +224 -0
- vortex/Jsonable.py +304 -0
- vortex/Payload.py +115 -0
- vortex/PayloadEndpoint.py +216 -0
- vortex/PayloadEndpointTest.py +164 -0
- vortex/PayloadEnvelope.py +166 -0
- vortex/PayloadFilterKeys.py +15 -0
- vortex/PayloadIO.py +152 -0
- vortex/PayloadPriority.py +11 -0
- vortex/PayloadResponse.py +173 -0
- vortex/PayloadResponseTest.py +39 -0
- vortex/PayloadTest.py +136 -0
- vortex/PerformTestActionTuple.py +30 -0
- vortex/SerialiseUtil.py +273 -0
- vortex/TestTuple.py +61 -0
- vortex/Tuple.py +968 -0
- vortex/TupleAction.py +72 -0
- vortex/TupleActionVortex.py +5 -0
- vortex/TupleDataForTest.py +25 -0
- vortex/TuplePerfTest.py +71 -0
- vortex/TuplePolymorphicTupleTypeFieldArgTest.py +74 -0
- vortex/TupleRestfulTest.py +140 -0
- vortex/TupleSelector.py +105 -0
- vortex/TupleTest.py +127 -0
- vortex/VortexABC.py +55 -0
- vortex/VortexClientHttp.py +308 -0
- vortex/VortexClientHttpTest.py +71 -0
- vortex/VortexClientTcp.py +355 -0
- vortex/VortexClientWebsocketFactory.py +383 -0
- vortex/VortexConnectionABC.py +88 -0
- vortex/VortexFactory.py +932 -0
- vortex/VortexPayloadProtocol.py +180 -0
- vortex/VortexServer.py +367 -0
- vortex/VortexServerConnection.py +127 -0
- vortex/VortexServerHttpResource.py +193 -0
- vortex/VortexServerTcp.py +104 -0
- vortex/VortexServerWebsocket.py +287 -0
- vortex/VortexTcpMemoryLeakPayloadTest.py +611 -0
- vortex/VortexTcpMemoryLeakRawTest.py +87 -0
- vortex/VortexTest.py +22 -0
- vortex/VortexUtil.py +314 -0
- vortex/VortexWritePushProducer.py +321 -0
- vortex/__init__.py +10 -0
- vortex/data_loader/TupleDataLiveLockManager.py +383 -0
- vortex/data_loader/TupleDataLoader.py +326 -0
- vortex/data_loader/TupleDataLoaderDelegate.py +90 -0
- vortex/data_loader/TupleDataLoaderTupleABC.py +10 -0
- vortex/data_loader/TupleDataLoaderTuples.py +53 -0
- vortex/data_loader/__init__.py +0 -0
- vortex/event_bus/TupleChangeEventABC.py +15 -0
- vortex/event_bus/TupleChangeEventBus.py +192 -0
- vortex/event_bus/TupleChangeEventBusObserverABC.py +11 -0
- vortex/event_bus/__init__.py +0 -0
- vortex/handler/TupleActionProcessor.py +270 -0
- vortex/handler/TupleActionProcessorProxy.py +218 -0
- vortex/handler/TupleActionProcessorTestHandler.py +27 -0
- vortex/handler/TupleDataActionClient.py +67 -0
- vortex/handler/TupleDataObservableCache.py +201 -0
- vortex/handler/TupleDataObservableHandler.py +371 -0
- vortex/handler/TupleDataObservableProxyHandler.py +340 -0
- vortex/handler/TupleDataObservableTestHandler.py +60 -0
- vortex/handler/TupleDataObserverClient.py +158 -0
- vortex/handler/VortexJSTupleLoaderTestHandler.py +42 -0
- vortex/handler/__init__.py +0 -0
- vortex/restful/GzippedDataHttpClient.py +170 -0
- vortex/restful/GzippedDataHttpClientTest.py +103 -0
- vortex/restful/GzippedPayloadHttpClient.py +181 -0
- vortex/restful/RestfulHttpClient.py +107 -0
- vortex/restful/RestfulHttpClientTest.py +60 -0
- vortex/restful/RestfulResource.py +153 -0
- vortex/restful/RestfulResourceTest.py +100 -0
- vortex/restful/__init__.py +0 -0
- vortex/rpc/RPC.py +446 -0
- vortex/rpc/RPCTest.py +12 -0
- vortex/rpc/__init__.py +0 -0
- vortex/sqla_orm/OrmCrudHandler.py +431 -0
- vortex/sqla_orm/TupleProviderForDb.py +42 -0
- vortex/sqla_orm/VortexTupleJson.py +24 -0
- vortex/sqla_orm/__init__.py +0 -0
- vortex/storage/TupleStorageSqlite.py +359 -0
- vortex/storage/__init__.py +0 -0
- vortexpy-4.0.23.dist-info/LICENSE +21 -0
- vortexpy-4.0.23.dist-info/METADATA +43 -0
- vortexpy-4.0.23.dist-info/RECORD +87 -0
- vortexpy-4.0.23.dist-info/WHEEL +5 -0
- vortexpy-4.0.23.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import logging
|
|
3
|
+
import struct
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import Union, List, Type
|
|
7
|
+
|
|
8
|
+
import pytz
|
|
9
|
+
from twisted.internet import reactor
|
|
10
|
+
from twisted.internet.defer import Deferred, inlineCallbacks
|
|
11
|
+
from twisted.internet.defer import succeed
|
|
12
|
+
from twisted.internet.protocol import Protocol
|
|
13
|
+
from twisted.web._newclient import ResponseDone
|
|
14
|
+
from twisted.web.client import Agent, GzipDecoder, ContentDecoderAgent
|
|
15
|
+
from twisted.web.http_headers import Headers
|
|
16
|
+
from twisted.web.iweb import IBodyProducer, UNKNOWN_LENGTH
|
|
17
|
+
from zope.interface import implementer
|
|
18
|
+
|
|
19
|
+
from vortex.Tuple import Tuple
|
|
20
|
+
from vortex.restful.RestfulResource import HTTP_REQUEST
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GzippedPayloadHttpClient:
|
|
26
|
+
"""A Restful http client that sends out a binary payload"""
|
|
27
|
+
|
|
28
|
+
DEFAULT_HEADERS = {
|
|
29
|
+
"User-Agent": ["synerty/1.0"],
|
|
30
|
+
"Content-Type": ["application/json"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
url: Union[str, bytes],
|
|
36
|
+
payload: bytes,
|
|
37
|
+
headers: dict[str, List[str]],
|
|
38
|
+
method: HTTP_REQUEST,
|
|
39
|
+
meta: Type[Tuple],
|
|
40
|
+
isPayloadGzipped: bool = True,
|
|
41
|
+
compressed: bool = True,
|
|
42
|
+
timeout: float = 10,
|
|
43
|
+
):
|
|
44
|
+
self._httpMethod = method.value.encode()
|
|
45
|
+
self._url = url.encode() if isinstance(url, str) else url
|
|
46
|
+
self._payload = payload
|
|
47
|
+
self._headers = {**self.DEFAULT_HEADERS, **headers}
|
|
48
|
+
self._meta = meta
|
|
49
|
+
self._compressed = (
|
|
50
|
+
compressed # send http requests whether gzip-compressed or not
|
|
51
|
+
)
|
|
52
|
+
self._isPayloadGzipped = isPayloadGzipped
|
|
53
|
+
self._timeout = timeout # connect timeout
|
|
54
|
+
|
|
55
|
+
@inlineCallbacks
|
|
56
|
+
def run(self) -> Type[Tuple]:
|
|
57
|
+
agent = Agent(reactor, connectTimeout=self._timeout)
|
|
58
|
+
# Add the gzip decoder
|
|
59
|
+
if self._compressed:
|
|
60
|
+
agent = ContentDecoderAgent(agent, [(b"gzip", GzipDecoder)])
|
|
61
|
+
|
|
62
|
+
binaryPayloadRequestProducer = _BinaryPayloadRequestProducer(
|
|
63
|
+
self._payload,
|
|
64
|
+
self._meta,
|
|
65
|
+
self._isPayloadGzipped,
|
|
66
|
+
)
|
|
67
|
+
# Make the web request
|
|
68
|
+
response = yield agent.request(
|
|
69
|
+
self._httpMethod,
|
|
70
|
+
self._url,
|
|
71
|
+
Headers(self._headers),
|
|
72
|
+
binaryPayloadRequestProducer,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self._meta = binaryPayloadRequestProducer.meta
|
|
76
|
+
self._meta.code = response.code
|
|
77
|
+
self._meta.version = response.version
|
|
78
|
+
self._meta.headers = {
|
|
79
|
+
k.decode(): v[0].decode()
|
|
80
|
+
for k, v in response.headers.getAllRawHeaders()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Get the responseTuple data
|
|
84
|
+
responseProducer = self._cbResponse(response, self._meta)
|
|
85
|
+
|
|
86
|
+
self._meta = responseProducer.meta
|
|
87
|
+
return self._meta
|
|
88
|
+
# return GzippedPayloadHttpResponse(
|
|
89
|
+
# code=response.code,
|
|
90
|
+
# version=response.version,
|
|
91
|
+
# headers={
|
|
92
|
+
# k.decode(): v[0].decode()
|
|
93
|
+
# for k, v in response.headers.getAllRawHeaders()
|
|
94
|
+
# },
|
|
95
|
+
# body=responseProducer.asyncData,
|
|
96
|
+
# requestDate=meta.requestDate,
|
|
97
|
+
# responseDate=meta.responseDate,
|
|
98
|
+
# )
|
|
99
|
+
|
|
100
|
+
def _cbResponse(self, response, meta):
|
|
101
|
+
responseProducer = _ResponseProducer(meta)
|
|
102
|
+
response.deliverBody(responseProducer)
|
|
103
|
+
return responseProducer
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@implementer(IBodyProducer)
|
|
107
|
+
class _BinaryPayloadRequestProducer:
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
payload: bytes,
|
|
111
|
+
meta: Type[Tuple],
|
|
112
|
+
isPayloadGzipped: bool = True,
|
|
113
|
+
):
|
|
114
|
+
self._meta = meta
|
|
115
|
+
self._payload = payload
|
|
116
|
+
self._isGzippedAlready = isPayloadGzipped
|
|
117
|
+
self.length = self._getGzipUncompressedSize()
|
|
118
|
+
self._gzipFile = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def meta(self):
|
|
122
|
+
return self._meta
|
|
123
|
+
|
|
124
|
+
def startProducing(self, consumer):
|
|
125
|
+
self._meta.requestDate = datetime.now(tz=pytz.utc)
|
|
126
|
+
|
|
127
|
+
if self._isGzippedAlready:
|
|
128
|
+
try:
|
|
129
|
+
decompressed = gzip.decompress(self._payload)
|
|
130
|
+
logger.info(f"declared payload length: {self.length}")
|
|
131
|
+
logger.info(f"decompressed payload length: {len(decompressed)}")
|
|
132
|
+
consumer.write(decompressed)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise e
|
|
135
|
+
else:
|
|
136
|
+
# uncompressed payload
|
|
137
|
+
consumer.write(self._payload)
|
|
138
|
+
return succeed(None)
|
|
139
|
+
|
|
140
|
+
def pauseProducing(self):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def stopProducing(self):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def _getGzipUncompressedSize(self) -> int:
|
|
147
|
+
# Uncompressed size is stored in the last 4 bytes of the gzip file.
|
|
148
|
+
# We can read the binary data and convert it to an int.
|
|
149
|
+
# (This will only work for files under 4GB)
|
|
150
|
+
with BytesIO(self._payload) as f:
|
|
151
|
+
try:
|
|
152
|
+
return struct.unpack("I", self._payload[-4:])[0]
|
|
153
|
+
except Exception as e:
|
|
154
|
+
# may be invalid format of gzip
|
|
155
|
+
logger.exception(e)
|
|
156
|
+
return UNKNOWN_LENGTH
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _ResponseProducer(Protocol):
|
|
160
|
+
def __init__(self, meta: Type[Tuple]):
|
|
161
|
+
self._finishedDeferred = Deferred()
|
|
162
|
+
self._writeSize = 0
|
|
163
|
+
self._body = b""
|
|
164
|
+
self._meta = meta
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def asyncData(self):
|
|
168
|
+
return self._finishedDeferred
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def meta(self):
|
|
172
|
+
return self._meta
|
|
173
|
+
|
|
174
|
+
def dataReceived(self, data: bytes):
|
|
175
|
+
logger.debug(bytes)
|
|
176
|
+
self._body += data
|
|
177
|
+
|
|
178
|
+
def connectionLost(self, reason):
|
|
179
|
+
if isinstance(reason.value, ResponseDone):
|
|
180
|
+
self._meta.responseDate = datetime.now(tz=pytz.utc)
|
|
181
|
+
self._meta.body = self._body
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Union, Type, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from twisted.internet import reactor
|
|
6
|
+
from twisted.internet.defer import Deferred, inlineCallbacks
|
|
7
|
+
from twisted.internet.protocol import Protocol
|
|
8
|
+
from twisted.web._newclient import ResponseDone
|
|
9
|
+
from twisted.web.client import Agent, GzipDecoder, ContentDecoderAgent
|
|
10
|
+
from twisted.web.http_headers import Headers
|
|
11
|
+
from zope.interface import implementer
|
|
12
|
+
from twisted.internet.defer import succeed
|
|
13
|
+
from twisted.web.iweb import IBodyProducer
|
|
14
|
+
|
|
15
|
+
from vortex.Tuple import Tuple
|
|
16
|
+
from vortex.restful.RestfulResource import HTTP_REQUEST
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RestfulHttpClient:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
url: Union[str, bytes],
|
|
25
|
+
method: HTTP_REQUEST,
|
|
26
|
+
postTuple: Tuple,
|
|
27
|
+
ResponseTuple: Type[Tuple],
|
|
28
|
+
):
|
|
29
|
+
self._httpMethod = method.value.encode()
|
|
30
|
+
self._url = url.encode() if isinstance(url, str) else url
|
|
31
|
+
self._postTuple = postTuple
|
|
32
|
+
self._ResponseTuple = ResponseTuple
|
|
33
|
+
|
|
34
|
+
@inlineCallbacks
|
|
35
|
+
def run(self):
|
|
36
|
+
# convert tuple to json
|
|
37
|
+
body = self._postTuple.tupleToRestfulJsonDict()
|
|
38
|
+
body = json.dumps(body).encode("utf-8")
|
|
39
|
+
|
|
40
|
+
# add http headers
|
|
41
|
+
headers = Headers(
|
|
42
|
+
{
|
|
43
|
+
"User-Agent": ["synerty/1.0"],
|
|
44
|
+
"Content-Type": ["application/json"],
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Add the gzip decoder
|
|
49
|
+
agent = ContentDecoderAgent(Agent(reactor), [(b"gzip", GzipDecoder)])
|
|
50
|
+
|
|
51
|
+
# Make the web request
|
|
52
|
+
response = yield agent.request(
|
|
53
|
+
self._httpMethod, self._url, headers, _BytesProducer(body)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Get the response data
|
|
57
|
+
responseData = yield self._cbResponse(response)
|
|
58
|
+
|
|
59
|
+
# Convert the bytes into a tuple and return
|
|
60
|
+
return self._parseTuple(responseData)
|
|
61
|
+
|
|
62
|
+
def _cbResponse(self, response):
|
|
63
|
+
bodyDownloader = _RestfulBody()
|
|
64
|
+
response.deliverBody(bodyDownloader)
|
|
65
|
+
return bodyDownloader.asyncData
|
|
66
|
+
|
|
67
|
+
def _parseTuple(self, bytes_):
|
|
68
|
+
json_ = json.loads(bytes_.decode("utf-8"))
|
|
69
|
+
return Tuple.restfulJsonDictToTupleWithValidation(
|
|
70
|
+
json_, self._ResponseTuple
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@implementer(IBodyProducer)
|
|
75
|
+
class _BytesProducer:
|
|
76
|
+
def __init__(self, body):
|
|
77
|
+
self.body = body
|
|
78
|
+
self.length = len(body)
|
|
79
|
+
|
|
80
|
+
def startProducing(self, consumer):
|
|
81
|
+
consumer.write(self.body)
|
|
82
|
+
return succeed(None)
|
|
83
|
+
|
|
84
|
+
def pauseProducing(self):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def stopProducing(self):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _RestfulBody(Protocol):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
self._finishedDeferred = Deferred()
|
|
94
|
+
self._writeSize = 0
|
|
95
|
+
self._body = b""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def asyncData(self):
|
|
99
|
+
return self._finishedDeferred
|
|
100
|
+
|
|
101
|
+
def dataReceived(self, data: bytes):
|
|
102
|
+
logger.debug(bytes)
|
|
103
|
+
self._body += data
|
|
104
|
+
|
|
105
|
+
def connectionLost(self, reason):
|
|
106
|
+
if isinstance(reason.value, ResponseDone):
|
|
107
|
+
self._finishedDeferred.callback(self._body)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Annotated, List
|
|
4
|
+
|
|
5
|
+
from twisted.trial import unittest
|
|
6
|
+
|
|
7
|
+
from vortex.Tuple import (
|
|
8
|
+
Tuple,
|
|
9
|
+
TupleField,
|
|
10
|
+
addTupleType,
|
|
11
|
+
IntTupleFieldValidator,
|
|
12
|
+
TupleFieldValidatorABC,
|
|
13
|
+
)
|
|
14
|
+
from vortex.restful.RestfulHttpClient import RestfulHttpClient
|
|
15
|
+
from vortex.restful.RestfulResource import HTTP_REQUEST
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@addTupleType
|
|
21
|
+
class TestTuple(Tuple):
|
|
22
|
+
__tupleType__ = "TestTuple"
|
|
23
|
+
id: int = TupleField()
|
|
24
|
+
count: Annotated[int, IntTupleFieldValidator(0, 100)] = TupleField()
|
|
25
|
+
text: str = TupleField()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@addTupleType
|
|
29
|
+
class ResponseTuple(Tuple):
|
|
30
|
+
__tupleType__ = "ResponseTuple"
|
|
31
|
+
data: str = TupleField(comment="request in string")
|
|
32
|
+
json: TestTuple = TupleField(comment="request converted to jsonDict")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RestfulClientTest(unittest.TestCase):
|
|
36
|
+
def setUp(self):
|
|
37
|
+
self.tuple = TestTuple()
|
|
38
|
+
self.tuple.id = 10
|
|
39
|
+
self.tuple.count = 50
|
|
40
|
+
self.tuple.text = "test"
|
|
41
|
+
|
|
42
|
+
def testHttpPost(self):
|
|
43
|
+
client = RestfulHttpClient(
|
|
44
|
+
"http://httpbin.org/anything",
|
|
45
|
+
method=HTTP_REQUEST.POST,
|
|
46
|
+
postTuple=self.tuple,
|
|
47
|
+
ResponseTuple=ResponseTuple,
|
|
48
|
+
)
|
|
49
|
+
d = client.run()
|
|
50
|
+
d.addCallback(self._checkPostJSON)
|
|
51
|
+
# d.addCallback(self.print)
|
|
52
|
+
return d
|
|
53
|
+
|
|
54
|
+
def _checkPostJSON(self, tuple_):
|
|
55
|
+
self.assertTrue(isinstance(tuple_, ResponseTuple))
|
|
56
|
+
self.assertEqual(
|
|
57
|
+
tuple_.data, json.dumps(self.tuple.tupleToRestfulJsonDict())
|
|
58
|
+
)
|
|
59
|
+
self.assertEqual(tuple_.json.id, self.tuple.id)
|
|
60
|
+
self.assertEqual(tuple_.json.text, self.tuple.text)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Callable
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from twisted.internet.defer import inlineCallbacks
|
|
8
|
+
from twisted.python.compat import nativeString
|
|
9
|
+
from twisted.web.server import NOT_DONE_YET
|
|
10
|
+
from txhttputil.site.BasicResource import BasicResource
|
|
11
|
+
from vortex import Tuple
|
|
12
|
+
from vortex.DeferUtil import deferToThreadWrapWithLogger
|
|
13
|
+
from vortex.DeferUtil import vortexLogFailure
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HTTP_REQUEST(Enum):
|
|
19
|
+
GET = "GET"
|
|
20
|
+
POST = "POST"
|
|
21
|
+
PUT = "PUT"
|
|
22
|
+
DELETE = "DELETE"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _JsonResource(BasicResource):
|
|
26
|
+
isLeaf = True
|
|
27
|
+
isGzipped = True
|
|
28
|
+
|
|
29
|
+
def __init__(self, handler: Callable, TupleClass: Tuple):
|
|
30
|
+
BasicResource.__init__(self)
|
|
31
|
+
|
|
32
|
+
self._handler = handler
|
|
33
|
+
self._tupleClass = TupleClass
|
|
34
|
+
|
|
35
|
+
@deferToThreadWrapWithLogger(logger)
|
|
36
|
+
def _requestToTuple(self, request) -> Tuple:
|
|
37
|
+
req = json.load(request.content)
|
|
38
|
+
tuple_ = self._tupleClass()
|
|
39
|
+
tuple_.fromJsonDict(req)
|
|
40
|
+
return tuple_
|
|
41
|
+
|
|
42
|
+
@deferToThreadWrapWithLogger(logger)
|
|
43
|
+
def _tupleToResponse(self, tuple_: Tuple) -> bytes:
|
|
44
|
+
dict_ = tuple_.tupleToRestfulJsonDict()
|
|
45
|
+
json_ = json.dumps(dict_)
|
|
46
|
+
return json_.encode()
|
|
47
|
+
|
|
48
|
+
def _writeSuccessResponse(self, response: bytes, request):
|
|
49
|
+
request.setResponseCode(200)
|
|
50
|
+
request.setHeader("Content-Type", "application/json")
|
|
51
|
+
request.write(response)
|
|
52
|
+
request.finish()
|
|
53
|
+
|
|
54
|
+
def _writeErrorResponse(self, request):
|
|
55
|
+
request.setResponseCode(500)
|
|
56
|
+
request.setHeader("Content-Type", "application/json")
|
|
57
|
+
request.write(b'{"error":"internal error"}')
|
|
58
|
+
request.finish()
|
|
59
|
+
|
|
60
|
+
@inlineCallbacks
|
|
61
|
+
def _renderAsync(self, request):
|
|
62
|
+
tupleIn = None
|
|
63
|
+
try:
|
|
64
|
+
tupleIn = yield self._requestToTuple(request)
|
|
65
|
+
tupleOut = yield self._handler(tupleIn)
|
|
66
|
+
response = yield self._tupleToResponse(tupleOut)
|
|
67
|
+
self._writeSuccessResponse(response, request)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
self._writeErrorResponse(request)
|
|
71
|
+
logger.debug(
|
|
72
|
+
f"Error while processing REST Tuple\n{tupleIn}\n{request.path}"
|
|
73
|
+
)
|
|
74
|
+
logger.exception(e)
|
|
75
|
+
|
|
76
|
+
def render(self, request):
|
|
77
|
+
d = self._renderAsync(request)
|
|
78
|
+
d.addErrback(vortexLogFailure, logger=logger, consumeError=True)
|
|
79
|
+
|
|
80
|
+
return NOT_DONE_YET
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ErrorJsonResource(BasicResource):
|
|
84
|
+
isLeaf = True
|
|
85
|
+
isGzipped = True
|
|
86
|
+
|
|
87
|
+
def __init__(self, errorCode: int):
|
|
88
|
+
self._errorCode = errorCode
|
|
89
|
+
|
|
90
|
+
def render(self, request):
|
|
91
|
+
request.setResponseCode(self._errorCode)
|
|
92
|
+
return b""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PluginRestfulResource(BasicResource):
|
|
96
|
+
def __init__(self):
|
|
97
|
+
BasicResource.__init__(self)
|
|
98
|
+
self._registeredMethods = {}
|
|
99
|
+
|
|
100
|
+
def registerMethod(
|
|
101
|
+
self,
|
|
102
|
+
handlerFunction: Callable,
|
|
103
|
+
TupleClass: Tuple,
|
|
104
|
+
urlPrefix: bytes,
|
|
105
|
+
registeredRequestMethod: List[HTTP_REQUEST],
|
|
106
|
+
):
|
|
107
|
+
if urlPrefix in self._registeredMethods:
|
|
108
|
+
raise ValueError(f'Route "{urlPrefix}" already exists')
|
|
109
|
+
if not callable(handlerFunction):
|
|
110
|
+
raise TypeError(f'"{str(handlerFunction)}" is not a callable')
|
|
111
|
+
|
|
112
|
+
# register handler as partial function (poor man's Template<type>)
|
|
113
|
+
# as callback function whose input is an instance of a Tuple
|
|
114
|
+
self._registeredMethods[urlPrefix] = {
|
|
115
|
+
"handler": handlerFunction,
|
|
116
|
+
"allowedMethods": set(
|
|
117
|
+
[method.value for method in registeredRequestMethod]
|
|
118
|
+
),
|
|
119
|
+
"tupleClass": TupleClass,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def getChild(self, path, request):
|
|
123
|
+
"""Get requests routed to an handler
|
|
124
|
+
|
|
125
|
+
Routes to a registered handler to process the request.
|
|
126
|
+
This function routes resources to handlers which eventually returns
|
|
127
|
+
JSON response resource in json. Everything returned here should be
|
|
128
|
+
final.
|
|
129
|
+
|
|
130
|
+
Once url prefix for a resource is matched, it checks the
|
|
131
|
+
request method is allowed. If granted, it delegates the request to the
|
|
132
|
+
registered handler to process the request. The handler should convert
|
|
133
|
+
request to a Tuple and should respond a tuple.
|
|
134
|
+
|
|
135
|
+
:param path: route url for a resource, with plugin name prefix stripped
|
|
136
|
+
:param request: twisted.web.http.Request
|
|
137
|
+
:return: twisted.web.resource.Resource
|
|
138
|
+
"""
|
|
139
|
+
# route check
|
|
140
|
+
if path not in self._registeredMethods.keys():
|
|
141
|
+
return ErrorJsonResource(404)
|
|
142
|
+
|
|
143
|
+
# request method check
|
|
144
|
+
requestMethod = nativeString(request.method)
|
|
145
|
+
if requestMethod not in self._registeredMethods[path]["allowedMethods"]:
|
|
146
|
+
return ErrorJsonResource(403)
|
|
147
|
+
|
|
148
|
+
# invoke resource handler
|
|
149
|
+
TupleClass = self._registeredMethods[path]["tupleClass"]
|
|
150
|
+
|
|
151
|
+
handler = self._registeredMethods[path]["handler"]
|
|
152
|
+
|
|
153
|
+
return _JsonResource(handler, TupleClass)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from vortex.restful.RestfulResource import HTTP_REQUEST
|
|
6
|
+
from vortex.restful.RestfulResource import PluginRestfulResource
|
|
7
|
+
from twisted.trial import unittest
|
|
8
|
+
from twisted.web.test.requesthelper import DummyRequest
|
|
9
|
+
from vortex.Tuple import Tuple
|
|
10
|
+
from vortex.Tuple import TupleField
|
|
11
|
+
from vortex.Tuple import addTupleType
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@addTupleType
|
|
17
|
+
class DummyTuple(Tuple):
|
|
18
|
+
__tupleType__ = "DummyTuple"
|
|
19
|
+
|
|
20
|
+
id = TupleField()
|
|
21
|
+
text = TupleField()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestHandlerController:
|
|
25
|
+
def testMethod(self, tuple_: DummyTuple):
|
|
26
|
+
if hasattr(tuple_, "tupleToRestfulJsonDict"):
|
|
27
|
+
# vortexpy > 2.1.3
|
|
28
|
+
return tuple_.tupleToRestfulJsonDict()
|
|
29
|
+
else:
|
|
30
|
+
return tuple_.tupleToSmallJsonDict()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RestfulServerTest(unittest.TestCase):
|
|
34
|
+
def setUp(self):
|
|
35
|
+
handlerController = TestHandlerController()
|
|
36
|
+
self.pluginAResource = PluginRestfulResource()
|
|
37
|
+
self.pluginAResource.registerMethod(
|
|
38
|
+
handlerController.testMethod,
|
|
39
|
+
DummyTuple,
|
|
40
|
+
b"test",
|
|
41
|
+
[HTTP_REQUEST.GET, HTTP_REQUEST.POST],
|
|
42
|
+
)
|
|
43
|
+
self.pluginAResource.registerMethod(
|
|
44
|
+
handlerController.testMethod,
|
|
45
|
+
DummyTuple,
|
|
46
|
+
b"test/test",
|
|
47
|
+
[HTTP_REQUEST.GET, HTTP_REQUEST.POST],
|
|
48
|
+
)
|
|
49
|
+
self.pluginAResource.registerMethod(
|
|
50
|
+
handlerController.testMethod,
|
|
51
|
+
DummyTuple,
|
|
52
|
+
b"test/test/test",
|
|
53
|
+
[HTTP_REQUEST.GET, HTTP_REQUEST.POST],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _dictToBytes(self, dictionary: dict) -> bytes:
|
|
57
|
+
return bytes(json.dumps(dictionary), "utf-8")
|
|
58
|
+
|
|
59
|
+
def _bytesToDict(self, bytes_: bytes) -> dict:
|
|
60
|
+
string = bytes_[0].decode("utf-8")
|
|
61
|
+
return json.loads(string)
|
|
62
|
+
|
|
63
|
+
def _check(self, expected: dict, actual: dict) -> bool:
|
|
64
|
+
# expected dict should be equal or a subnet of actual dict
|
|
65
|
+
return expected.items() <= actual.items()
|
|
66
|
+
|
|
67
|
+
def testValidJsonRequest(self):
|
|
68
|
+
requestDict = {"id": 1, "text": "text"}
|
|
69
|
+
|
|
70
|
+
request = DummyRequest([])
|
|
71
|
+
request.content = io.BytesIO(self._dictToBytes(requestDict))
|
|
72
|
+
|
|
73
|
+
jsonResource = self.pluginAResource.getChild(b"test", request)
|
|
74
|
+
|
|
75
|
+
jsonResource.render(request)
|
|
76
|
+
self.assertEqual(request.responseCode, 200)
|
|
77
|
+
responseDict = self._bytesToDict(request.written)
|
|
78
|
+
self.assertTrue(self._check(requestDict, responseDict))
|
|
79
|
+
|
|
80
|
+
def testInvalidJsonRequest(self):
|
|
81
|
+
request = DummyRequest([])
|
|
82
|
+
request.content = io.BytesIO(b"##invalid json}")
|
|
83
|
+
|
|
84
|
+
jsonResource = self.pluginAResource.getChild(b"test", request)
|
|
85
|
+
jsonResource.render(request)
|
|
86
|
+
self.assertEqual(request.responseCode, 500)
|
|
87
|
+
|
|
88
|
+
def testPath(self):
|
|
89
|
+
requestDict = {"id": 1, "text": "text"}
|
|
90
|
+
|
|
91
|
+
for path in [b"test", b"test/test", b"test/test/test"]:
|
|
92
|
+
request = DummyRequest([])
|
|
93
|
+
request.content = io.BytesIO(self._dictToBytes(requestDict))
|
|
94
|
+
|
|
95
|
+
jsonResource = self.pluginAResource.getChild(path, request)
|
|
96
|
+
|
|
97
|
+
jsonResource.render(request)
|
|
98
|
+
self.assertEqual(request.responseCode, 200)
|
|
99
|
+
responseDict = self._bytesToDict(request.written)
|
|
100
|
+
self.assertTrue(self._check(requestDict, responseDict))
|
|
File without changes
|