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,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
import pytz
|
|
5
|
+
from twisted.internet import task
|
|
6
|
+
|
|
7
|
+
from vortex.DeferUtil import deferToThreadWrapWithLogger
|
|
8
|
+
from vortex.Payload import Payload
|
|
9
|
+
from vortex.TestTuple import TestTuple
|
|
10
|
+
from vortex.TupleDataForTest import makeTestTupleData
|
|
11
|
+
from vortex.TupleSelector import TupleSelector
|
|
12
|
+
from vortex.handler.TupleDataObservableHandler import (
|
|
13
|
+
TuplesProviderABC,
|
|
14
|
+
TupleDataObservableHandler,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
testTuples1Selector = TupleSelector("testTuples1", {"count": 4})
|
|
19
|
+
|
|
20
|
+
testTuples2Selector = TupleSelector("testTuples2", {"count": 7})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestTupleProvider(TuplesProviderABC):
|
|
24
|
+
@deferToThreadWrapWithLogger(logger)
|
|
25
|
+
def makeVortexMsg(self, filt: dict, tupleSelector: TupleSelector) -> bytes:
|
|
26
|
+
count = tupleSelector.selector["count"]
|
|
27
|
+
tuples = makeTestTupleData(count)
|
|
28
|
+
|
|
29
|
+
for t in tuples:
|
|
30
|
+
t.aDate = datetime.now(pytz.utc)
|
|
31
|
+
t.aDict = tupleSelector.selector
|
|
32
|
+
t.aString = tupleSelector.name
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
Payload(filt=filt, tuples=tuples)
|
|
36
|
+
.makePayloadEnvelope()
|
|
37
|
+
.toVortexMsg()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotifyTestTimer:
|
|
42
|
+
@classmethod
|
|
43
|
+
def __notify(cls):
|
|
44
|
+
observableHandler.notifyOfTupleUpdate(testTuples1Selector)
|
|
45
|
+
observableHandler.notifyOfTupleUpdate(testTuples2Selector)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def startTupleUpdateNotifyer(cls):
|
|
49
|
+
cls.__loopingCall = task.LoopingCall(cls.__notify)
|
|
50
|
+
d = cls.__loopingCall.start(2)
|
|
51
|
+
d.addErrback(lambda f: logger.exception(f.value))
|
|
52
|
+
d.addCallback(
|
|
53
|
+
lambda _: logger.debug("Observable tuple updates started")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
observableHandler = TupleDataObservableHandler("vortexTestObservable")
|
|
58
|
+
observableHandler.addTupleProvider(TestTuple.tupleName(), TestTupleProvider())
|
|
59
|
+
observableHandler.addTupleProvider("testTuples1", TestTupleProvider())
|
|
60
|
+
observableHandler.addTupleProvider("testTuples2", TestTupleProvider())
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from copy import copy
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from reactivex.subject import Subject
|
|
6
|
+
from reactivex import operators as ops
|
|
7
|
+
from twisted.internet.defer import Deferred, inlineCallbacks
|
|
8
|
+
|
|
9
|
+
from vortex.DeferUtil import vortexLogFailure
|
|
10
|
+
from vortex.Payload import Payload
|
|
11
|
+
from vortex.PayloadEndpoint import PayloadEndpoint
|
|
12
|
+
from vortex.PayloadEnvelope import PayloadEnvelope
|
|
13
|
+
from vortex.PayloadResponse import PayloadResponse
|
|
14
|
+
from vortex.TupleSelector import TupleSelector
|
|
15
|
+
from vortex.VortexFactory import VortexFactory
|
|
16
|
+
from vortex.handler.TupleDataObservableCache import TupleDataObservableCache
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TupleDataObserverClient(TupleDataObservableCache):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
destVortexName,
|
|
25
|
+
observableName,
|
|
26
|
+
additionalFilt=None,
|
|
27
|
+
observerName="default",
|
|
28
|
+
):
|
|
29
|
+
"""Constructor
|
|
30
|
+
|
|
31
|
+
:param observableName: The name of this observable
|
|
32
|
+
:param additionalFilt: Any additional filter keys that are required
|
|
33
|
+
:param destVortexName: The dest vortex name to send the payloads to
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
TupleDataObservableCache.__init__(self)
|
|
37
|
+
|
|
38
|
+
self._destVortexName = destVortexName
|
|
39
|
+
self._observableName = observableName
|
|
40
|
+
self._observerName = observerName
|
|
41
|
+
|
|
42
|
+
self._sendFilt = dict(
|
|
43
|
+
name=observableName,
|
|
44
|
+
observerName=observerName,
|
|
45
|
+
key="tupleDataObservable",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self._listenFilt = dict(name=observableName, key="tupleDataObservable")
|
|
49
|
+
|
|
50
|
+
if additionalFilt:
|
|
51
|
+
self._sendFilt.update(additionalFilt)
|
|
52
|
+
self._listenFilt.update(additionalFilt)
|
|
53
|
+
|
|
54
|
+
self._endpoint = PayloadEndpoint(
|
|
55
|
+
self._listenFilt,
|
|
56
|
+
self._receivePayload,
|
|
57
|
+
acceptOnlyFromVortex=destVortexName,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# There are no online checks for the vortex
|
|
61
|
+
# isOnlineSub = statusService.isOnline
|
|
62
|
+
# .filter(online= > online == = true)
|
|
63
|
+
# .subscribe(online= > self.vortexOnlineChanged())
|
|
64
|
+
#
|
|
65
|
+
# self.onDestroyEvent.subscribe(() = > isOnlineSub.unsubscribe())
|
|
66
|
+
VortexFactory.subscribeToVortexStatusChange(destVortexName).pipe(ops.filter(
|
|
67
|
+
lambda online: online is True
|
|
68
|
+
)).subscribe(self._vortexOnlineChanged)
|
|
69
|
+
|
|
70
|
+
TupleDataObservableCache.start(self)
|
|
71
|
+
|
|
72
|
+
def shutdown(self):
|
|
73
|
+
self._endpoint.shutdown()
|
|
74
|
+
TupleDataObservableCache.shutdown(self)
|
|
75
|
+
|
|
76
|
+
def pollForTuples(
|
|
77
|
+
self, tupleSelector: TupleSelector, logTimeoutError: bool = True
|
|
78
|
+
) -> Deferred:
|
|
79
|
+
startFilt = copy(self._sendFilt)
|
|
80
|
+
startFilt.update({"subscribe": False, "tupleSelector": tupleSelector})
|
|
81
|
+
|
|
82
|
+
def updateCacheCallback(
|
|
83
|
+
payloadEnvelope: PayloadEnvelope,
|
|
84
|
+
) -> PayloadEnvelope:
|
|
85
|
+
cache, _ = self._updateCache(payloadEnvelope)
|
|
86
|
+
return payloadEnvelope
|
|
87
|
+
|
|
88
|
+
pr = PayloadResponse(
|
|
89
|
+
payloadEnvelope=PayloadEnvelope(startFilt),
|
|
90
|
+
destVortexName=self._destVortexName,
|
|
91
|
+
logTimeoutError=logTimeoutError,
|
|
92
|
+
)
|
|
93
|
+
pr.addCallback(updateCacheCallback)
|
|
94
|
+
pr.addCallback(
|
|
95
|
+
lambda payloadEnvelope: payloadEnvelope.decodePayloadDefer()
|
|
96
|
+
)
|
|
97
|
+
pr.addCallback(lambda payload: payload.tuples)
|
|
98
|
+
return pr
|
|
99
|
+
|
|
100
|
+
def subscribeToTupleSelector(self, tupleSelector: TupleSelector) -> Subject:
|
|
101
|
+
cache = self._makeCache(tupleSelector)
|
|
102
|
+
self._tellServerWeWantData([tupleSelector])
|
|
103
|
+
return cache.subject
|
|
104
|
+
|
|
105
|
+
def _vortexOnlineChanged(self, *args) -> None:
|
|
106
|
+
self._tellServerWeWantData(self._tupleSelectors())
|
|
107
|
+
|
|
108
|
+
@inlineCallbacks
|
|
109
|
+
def _receivePayload(self, payloadEnvelope: PayloadEnvelope, **kwargs):
|
|
110
|
+
# If this message is for a specific observer, and it's not us, then discard it.
|
|
111
|
+
filtObserverName = payloadEnvelope.filt.get("observerName")
|
|
112
|
+
if (
|
|
113
|
+
filtObserverName is not None
|
|
114
|
+
and filtObserverName != self._observerName
|
|
115
|
+
):
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# If this message is an error response, then don't process it.
|
|
119
|
+
if payloadEnvelope.result not in (None, True):
|
|
120
|
+
logger.error(
|
|
121
|
+
"Vortex responded with error : %s" % payloadEnvelope.result
|
|
122
|
+
)
|
|
123
|
+
logger.error(str(payloadEnvelope.filt))
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
tupleSelector: TupleSelector = payloadEnvelope.filt["tupleSelector"]
|
|
127
|
+
|
|
128
|
+
if not self._hasTupleSelector(tupleSelector):
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
cache, requiredUpdate = self._updateCache(payloadEnvelope)
|
|
132
|
+
if not requiredUpdate:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
payload = yield Payload().fromEncodedPayloadDefer(cache.encodedPayload)
|
|
136
|
+
cache.subject.on_next(payload.tuples)
|
|
137
|
+
|
|
138
|
+
def _tellServerWeWantData(self, tupleSelectors: List[TupleSelector]):
|
|
139
|
+
for tupleSelector in tupleSelectors:
|
|
140
|
+
self._sendRequestToServer(
|
|
141
|
+
PayloadEnvelope(
|
|
142
|
+
{"subscribe": True, "tupleSelector": tupleSelector}
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _sendRequestToServer(self, payload):
|
|
147
|
+
payload.filt.update(self._sendFilt)
|
|
148
|
+
d = VortexFactory.sendVortexMsg(
|
|
149
|
+
vortexMsgs=payload.toVortexMsg(),
|
|
150
|
+
destVortexName=self._destVortexName,
|
|
151
|
+
)
|
|
152
|
+
d.addErrback(vortexLogFailure, logger, consumeError=True)
|
|
153
|
+
|
|
154
|
+
def _sendUnsubscribeToServer(self, tupleSelector: TupleSelector):
|
|
155
|
+
payload = PayloadEnvelope()
|
|
156
|
+
payload.filt["tupleSelector"] = tupleSelector
|
|
157
|
+
payload.filt["unsubscribe"] = True
|
|
158
|
+
self._sendRequestToServer(payload)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# import logging
|
|
2
|
+
#
|
|
3
|
+
# from vortex.PayloadFilterKeys import plDeleteKey
|
|
4
|
+
# from vortex.test.TestTuple import TestTuple
|
|
5
|
+
# from vortex.test.TupleDataForTest import makeTestTupleData
|
|
6
|
+
#
|
|
7
|
+
# logger = logging.getLogger(__name__)
|
|
8
|
+
#
|
|
9
|
+
#
|
|
10
|
+
# # If there were reallying going to a DB, we'd use OrmCrudHandler
|
|
11
|
+
# class VortexJSTupleLoaderTestHandler(ModelHandler):
|
|
12
|
+
# def buildModel(self, payload, **kwargs):
|
|
13
|
+
# logger.debug("Received payload with %s tuples and filt=%s",
|
|
14
|
+
# len(payload.tuples), payload.filt)
|
|
15
|
+
#
|
|
16
|
+
# data = []
|
|
17
|
+
#
|
|
18
|
+
# if payload.tuples:
|
|
19
|
+
# # Return nothing if this was a delete
|
|
20
|
+
# if plDeleteKey in payload.filt:
|
|
21
|
+
# # Return nothing, it was deleted
|
|
22
|
+
# pass
|
|
23
|
+
#
|
|
24
|
+
# else:
|
|
25
|
+
# # Else this was a save, just update some data and return id
|
|
26
|
+
# data = payload.tuples
|
|
27
|
+
# for testTuple in data:
|
|
28
|
+
# testTuple.aInt = (testTuple.aInt if testTuple.aInt else 0) + 10
|
|
29
|
+
# testTuple.aBoolTrue = not (testTuple.aBoolTrue
|
|
30
|
+
# if testTuple.aBoolTrue
|
|
31
|
+
# else 0)
|
|
32
|
+
#
|
|
33
|
+
# else:
|
|
34
|
+
# # Else this is to get new data.
|
|
35
|
+
# data = makeTestTupleData()
|
|
36
|
+
#
|
|
37
|
+
# return data
|
|
38
|
+
#
|
|
39
|
+
#
|
|
40
|
+
# __handler = VortexJSTupleLoaderTestHandler({
|
|
41
|
+
# "key": "vortex.tuple-loader.test.data"
|
|
42
|
+
# })
|
|
File without changes
|
|
@@ -0,0 +1,170 @@
|
|
|
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 GzippedDataHttpClient:
|
|
26
|
+
"""A Restful http client that sends out a binary payload"""
|
|
27
|
+
|
|
28
|
+
DEFAULT_HEADERS = {
|
|
29
|
+
"User-Agent": ["vortex/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
|
+
try:
|
|
69
|
+
response = yield agent.request(
|
|
70
|
+
self._httpMethod,
|
|
71
|
+
self._url,
|
|
72
|
+
Headers(self._headers),
|
|
73
|
+
binaryPayloadRequestProducer,
|
|
74
|
+
)
|
|
75
|
+
self._meta = binaryPayloadRequestProducer.meta
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self._meta.exceptions.append(e)
|
|
78
|
+
finally:
|
|
79
|
+
if self._meta.exceptions:
|
|
80
|
+
return self._meta
|
|
81
|
+
|
|
82
|
+
self._meta.code = response.code
|
|
83
|
+
self._meta.version = response.version
|
|
84
|
+
self._meta.headers = {
|
|
85
|
+
k.decode(): v[0].decode()
|
|
86
|
+
for k, v in response.headers.getAllRawHeaders()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
self._meta = yield self._cbResponse(response, self._meta)
|
|
90
|
+
return self._meta
|
|
91
|
+
|
|
92
|
+
def _cbResponse(self, response, meta):
|
|
93
|
+
responseProducer = _ResponseProducer(meta)
|
|
94
|
+
response.deliverBody(responseProducer)
|
|
95
|
+
return responseProducer.asyncMeta
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@implementer(IBodyProducer)
|
|
99
|
+
class _BinaryPayloadRequestProducer:
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
payload: bytes,
|
|
103
|
+
meta: Type[Tuple],
|
|
104
|
+
isPayloadGzipped: bool = True,
|
|
105
|
+
):
|
|
106
|
+
self._meta = meta
|
|
107
|
+
self._payload = payload
|
|
108
|
+
self._isGzippedAlready = isPayloadGzipped
|
|
109
|
+
self.length = self._getGzipUncompressedSize()
|
|
110
|
+
self._gzipFile = None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def meta(self):
|
|
114
|
+
return self._meta
|
|
115
|
+
|
|
116
|
+
def startProducing(self, consumer):
|
|
117
|
+
self._meta.requestDate = datetime.now(tz=pytz.utc)
|
|
118
|
+
|
|
119
|
+
payload = self._payload
|
|
120
|
+
if self._isGzippedAlready:
|
|
121
|
+
payload = gzip.decompress(self._payload)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
consumer.write(payload)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise e
|
|
127
|
+
|
|
128
|
+
return succeed(None)
|
|
129
|
+
|
|
130
|
+
def pauseProducing(self):
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def stopProducing(self):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def _getGzipUncompressedSize(self) -> int:
|
|
137
|
+
# Uncompressed size is stored in the last 4 bytes of the gzip file.
|
|
138
|
+
# We can read the binary data and convert it to an int.
|
|
139
|
+
# (This will only work for files under 4GB)
|
|
140
|
+
if not self._isGzippedAlready:
|
|
141
|
+
return len(self._payload)
|
|
142
|
+
|
|
143
|
+
with BytesIO(self._payload) as f:
|
|
144
|
+
try:
|
|
145
|
+
return struct.unpack("I", self._payload[-4:])[0]
|
|
146
|
+
except Exception as e:
|
|
147
|
+
# may be invalid format of gzip
|
|
148
|
+
logger.exception(e)
|
|
149
|
+
return UNKNOWN_LENGTH
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _ResponseProducer(Protocol):
|
|
153
|
+
def __init__(self, meta: Type[Tuple]):
|
|
154
|
+
self._finishedDeferred = Deferred()
|
|
155
|
+
self._writeSize = 0
|
|
156
|
+
self._body = b""
|
|
157
|
+
self._meta = meta
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def asyncMeta(self):
|
|
161
|
+
return self._finishedDeferred
|
|
162
|
+
|
|
163
|
+
def dataReceived(self, data: bytes):
|
|
164
|
+
self._body += data
|
|
165
|
+
|
|
166
|
+
def connectionLost(self, reason):
|
|
167
|
+
if isinstance(reason.value, ResponseDone):
|
|
168
|
+
self._meta.responseDate = datetime.now(tz=pytz.utc)
|
|
169
|
+
self._meta.body = self._body
|
|
170
|
+
self._finishedDeferred.callback(self._meta)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from twisted.trial import unittest
|
|
7
|
+
from twisted.internet.error import TimeoutError as TwistedTimeoutError
|
|
8
|
+
|
|
9
|
+
from vortex.Tuple import Tuple, TupleField, addTupleType
|
|
10
|
+
from vortex.restful.GzippedDataHttpClient import GzippedDataHttpClient
|
|
11
|
+
from vortex.restful.RestfulResource import HTTP_REQUEST
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@addTupleType
|
|
17
|
+
class GzippedDataHttpResponseTuple(Tuple):
|
|
18
|
+
__tupleType__ = "GzippedDataHttpResponseTuple"
|
|
19
|
+
|
|
20
|
+
requestDate: datetime = TupleField()
|
|
21
|
+
responseDate: datetime = TupleField()
|
|
22
|
+
code: int = TupleField(defaultValue=0)
|
|
23
|
+
version: tuple = TupleField() # (b'HTTP', 1, 1)
|
|
24
|
+
headers: dict[str, str] = TupleField()
|
|
25
|
+
body: bytes = TupleField()
|
|
26
|
+
exceptions: List[Exception] = TupleField(defaultValue=[])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GzippedDataHttpClientTest(unittest.TestCase):
|
|
30
|
+
def setUp(self):
|
|
31
|
+
self.payload = gzip.compress(b"test")
|
|
32
|
+
self.headers = {}
|
|
33
|
+
self.meta = GzippedDataHttpResponseTuple()
|
|
34
|
+
|
|
35
|
+
def testHttpPost(self):
|
|
36
|
+
client = GzippedDataHttpClient(
|
|
37
|
+
url="http://httpbin.org/post",
|
|
38
|
+
payload=self.payload,
|
|
39
|
+
headers=self.headers,
|
|
40
|
+
method=HTTP_REQUEST.POST,
|
|
41
|
+
meta=GzippedDataHttpResponseTuple(),
|
|
42
|
+
isPayloadGzipped=True,
|
|
43
|
+
compressed=True,
|
|
44
|
+
)
|
|
45
|
+
d = client.run()
|
|
46
|
+
d.addCallback(self._assertValidResponse)
|
|
47
|
+
return d
|
|
48
|
+
|
|
49
|
+
def testHttpsPost(self):
|
|
50
|
+
client = GzippedDataHttpClient(
|
|
51
|
+
url="https://httpbin.org/post",
|
|
52
|
+
payload=self.payload,
|
|
53
|
+
headers=self.headers,
|
|
54
|
+
method=HTTP_REQUEST.POST,
|
|
55
|
+
meta=GzippedDataHttpResponseTuple(),
|
|
56
|
+
isPayloadGzipped=True,
|
|
57
|
+
compressed=True,
|
|
58
|
+
)
|
|
59
|
+
d = client.run()
|
|
60
|
+
d.addCallback(self._assertValidResponse)
|
|
61
|
+
return d
|
|
62
|
+
|
|
63
|
+
def testPlaintextDataPost(self):
|
|
64
|
+
payload = gzip.decompress(self.payload)
|
|
65
|
+
client = GzippedDataHttpClient(
|
|
66
|
+
url="http://httpbin.org/post",
|
|
67
|
+
payload=payload,
|
|
68
|
+
headers=self.headers,
|
|
69
|
+
method=HTTP_REQUEST.POST,
|
|
70
|
+
meta=GzippedDataHttpResponseTuple(),
|
|
71
|
+
isPayloadGzipped=False,
|
|
72
|
+
compressed=True,
|
|
73
|
+
)
|
|
74
|
+
d = client.run()
|
|
75
|
+
d.addCallback(self._assertValidResponse)
|
|
76
|
+
return d
|
|
77
|
+
|
|
78
|
+
def testConnectTimeout(self):
|
|
79
|
+
client = GzippedDataHttpClient(
|
|
80
|
+
url="http://example.com:19839",
|
|
81
|
+
payload=self.payload,
|
|
82
|
+
headers=self.headers,
|
|
83
|
+
method=HTTP_REQUEST.POST,
|
|
84
|
+
meta=GzippedDataHttpResponseTuple(),
|
|
85
|
+
isPayloadGzipped=False,
|
|
86
|
+
compressed=True,
|
|
87
|
+
timeout=1.0,
|
|
88
|
+
)
|
|
89
|
+
d = client.run()
|
|
90
|
+
d.addCallback(self._assertTimeoutException)
|
|
91
|
+
return d
|
|
92
|
+
|
|
93
|
+
def _assertValidResponse(self, tuple_: GzippedDataHttpResponseTuple):
|
|
94
|
+
self.assertEqual(tuple_.code, 200)
|
|
95
|
+
self.assertTrue(
|
|
96
|
+
(tuple_.responseDate - tuple_.requestDate).total_seconds() > 0
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _assertTimeoutException(self, tuple_: GzippedDataHttpResponseTuple):
|
|
100
|
+
booleans = [
|
|
101
|
+
isinstance(e, TwistedTimeoutError) for e in tuple_.exceptions
|
|
102
|
+
]
|
|
103
|
+
self.assertTrue(any(booleans))
|