updates
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from paypalhttp.environment import Environment
|
||||
from paypalhttp.file import File
|
||||
from paypalhttp.http_client import HttpClient
|
||||
from paypalhttp.http_response import HttpResponse
|
||||
from paypalhttp.http_error import HttpError
|
||||
from paypalhttp.serializers import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,54 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
class Encoder(object):
|
||||
|
||||
def __init__(self, encoders):
|
||||
self.encoders = encoders
|
||||
|
||||
def serialize_request(self, httprequest):
|
||||
if hasattr(httprequest, "headers"):
|
||||
if "content-type" in httprequest.headers:
|
||||
contenttype = httprequest.headers["content-type"]
|
||||
enc = self._encoder(contenttype)
|
||||
if enc:
|
||||
return enc.encode(httprequest)
|
||||
else:
|
||||
message = "Unable to serialize request with Content-Type {0}. Supported encodings are {1}".format(
|
||||
contenttype, self.supported_encodings())
|
||||
print(message)
|
||||
raise IOError(message)
|
||||
else:
|
||||
message = "Http request does not have content-type header set"
|
||||
print(message)
|
||||
raise IOError(message)
|
||||
|
||||
def deserialize_response(self, response_body, headers):
|
||||
if headers and "content-type" in headers:
|
||||
contenttype = headers["content-type"].lower()
|
||||
enc = self._encoder(contenttype)
|
||||
if enc:
|
||||
return enc.decode(response_body)
|
||||
else:
|
||||
message = "Unable to deserialize response with content-type {0}. Supported decodings are {1}".format(
|
||||
contenttype, self.supported_encodings())
|
||||
print(message)
|
||||
raise IOError(message)
|
||||
else:
|
||||
message = "Http response does not have content-type header set"
|
||||
print(message)
|
||||
raise IOError(message)
|
||||
|
||||
|
||||
def supported_encodings(self):
|
||||
return [enc.content_type() for enc in self.encoders]
|
||||
|
||||
def _encoder(self, content_type):
|
||||
for enc in self.encoders:
|
||||
if re.match(enc.content_type(), content_type) is not None:
|
||||
return enc
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
class Environment(object):
|
||||
def __init__(self, base_url):
|
||||
self.base_url = base_url
|
||||
|
||||
35
Backend/venv/lib/python3.12/site-packages/paypalhttp/file.py
Normal file
35
Backend/venv/lib/python3.12/site-packages/paypalhttp/file.py
Normal file
@@ -0,0 +1,35 @@
|
||||
class File(object):
|
||||
|
||||
@classmethod
|
||||
def fromhandle(cls, handle):
|
||||
return File(handle.name, handle.mode)
|
||||
|
||||
def __init__(self, name, mode='rb'):
|
||||
self._handle = None
|
||||
self._data = None
|
||||
|
||||
self.mode = mode
|
||||
self.closed = False
|
||||
self.name = name
|
||||
|
||||
def read(self):
|
||||
self.open()
|
||||
|
||||
if self._data:
|
||||
return self._data
|
||||
else:
|
||||
self._data = self._handle.read()
|
||||
return self._data
|
||||
|
||||
def close(self):
|
||||
if self._handle:
|
||||
self._handle.close()
|
||||
self._handle = None
|
||||
self.closed = True
|
||||
|
||||
def open(self):
|
||||
if not self._handle:
|
||||
if not self.closed:
|
||||
self._handle = open(self.name, self.mode)
|
||||
else:
|
||||
raise IOError('Open of closed file')
|
||||
@@ -0,0 +1,81 @@
|
||||
import requests
|
||||
import copy
|
||||
|
||||
from paypalhttp.encoder import Encoder
|
||||
from paypalhttp.http_response import HttpResponse
|
||||
from paypalhttp.http_error import HttpError
|
||||
from paypalhttp.serializers import Json, Text, Multipart, FormEncoded
|
||||
|
||||
|
||||
class HttpClient(object):
|
||||
|
||||
def __init__(self, environment):
|
||||
self._injectors = []
|
||||
self.environment = environment
|
||||
self.encoder = Encoder([Json(), Text(), Multipart(), FormEncoded()])
|
||||
|
||||
def get_user_agent(self):
|
||||
return "Python HTTP/1.1"
|
||||
|
||||
def get_timeout(self):
|
||||
return 30
|
||||
|
||||
def add_injector(self, injector):
|
||||
if injector and '__call__' in dir(injector):
|
||||
self._injectors.append(injector)
|
||||
else:
|
||||
message = "injector must be a function or implement the __call__ method"
|
||||
print(message)
|
||||
raise TypeError(message)
|
||||
|
||||
def execute(self, request):
|
||||
reqCpy = copy.deepcopy(request)
|
||||
|
||||
try:
|
||||
getattr(reqCpy, 'headers')
|
||||
except AttributeError:
|
||||
reqCpy.headers = {}
|
||||
|
||||
for injector in self._injectors:
|
||||
injector(reqCpy)
|
||||
|
||||
data = None
|
||||
|
||||
formatted_headers = self.format_headers(reqCpy.headers)
|
||||
|
||||
if "user-agent" not in formatted_headers:
|
||||
reqCpy.headers["user-agent"] = self.get_user_agent()
|
||||
|
||||
if hasattr(reqCpy, 'body') and reqCpy.body is not None:
|
||||
raw_headers = reqCpy.headers
|
||||
reqCpy.headers = formatted_headers
|
||||
data = self.encoder.serialize_request(reqCpy)
|
||||
reqCpy.headers = self.map_headers(raw_headers, formatted_headers)
|
||||
|
||||
resp = requests.request(method=reqCpy.verb,
|
||||
url=self.environment.base_url + reqCpy.path,
|
||||
headers=reqCpy.headers,
|
||||
data=data)
|
||||
|
||||
return self.parse_response(resp)
|
||||
|
||||
def format_headers(self, headers):
|
||||
return dict((k.lower(), v) for k, v in headers.items())
|
||||
|
||||
def map_headers(self, raw_headers, formatted_headers):
|
||||
for header_name in raw_headers:
|
||||
if header_name.lower() in formatted_headers:
|
||||
raw_headers[header_name] = formatted_headers[header_name.lower()]
|
||||
return raw_headers
|
||||
|
||||
def parse_response(self, response):
|
||||
status_code = response.status_code
|
||||
|
||||
if 200 <= status_code <= 299:
|
||||
body = ""
|
||||
if response.text and (len(response.text) > 0 and response.text != 'None'):
|
||||
body = self.encoder.deserialize_response(response.text, self.format_headers(response.headers))
|
||||
|
||||
return HttpResponse(body, response.status_code, response.headers)
|
||||
else:
|
||||
raise HttpError(response.text, response.status_code, response.headers)
|
||||
@@ -0,0 +1,10 @@
|
||||
class HttpError(IOError):
|
||||
|
||||
def __init__(self, message, status_code, headers):
|
||||
IOError.__init__(self)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
@@ -0,0 +1,66 @@
|
||||
def setattr_mixed(dest, key, value):
|
||||
if isinstance(dest, list):
|
||||
dest.append(value)
|
||||
else:
|
||||
setattr(dest, key, value)
|
||||
|
||||
|
||||
def construct_object(name, data, cls=object):
|
||||
if isinstance(data, dict):
|
||||
iterator = iter(data)
|
||||
dest = Result(data)
|
||||
elif isinstance(data, list):
|
||||
iterator = range(len(data))
|
||||
dest = []
|
||||
else:
|
||||
return data
|
||||
|
||||
for k in iterator:
|
||||
v = data[k]
|
||||
|
||||
k = str(k).replace("-", "_").lower()
|
||||
if isinstance(v, dict):
|
||||
setattr_mixed(dest, k, construct_object(k, v))
|
||||
elif isinstance(v, list):
|
||||
l = []
|
||||
for i in range(len(v)):
|
||||
setattr_mixed(l, i, construct_object(k, v[i]))
|
||||
|
||||
setattr_mixed(dest, k, l)
|
||||
else:
|
||||
setattr_mixed(dest, k, v)
|
||||
|
||||
return dest
|
||||
|
||||
|
||||
class Result(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self._dict = data;
|
||||
|
||||
def dict(self):
|
||||
return self._dict
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._dict
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._dict[key]
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
|
||||
def __init__(self, data, status_code, headers=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
if data and len(data) > 0:
|
||||
if isinstance(data, str):
|
||||
self.result = data
|
||||
elif isinstance(data, dict) or isinstance(data, list):
|
||||
self.result = construct_object('Result', data) # todo: pass through response type
|
||||
else:
|
||||
self.result = None
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from paypalhttp.serializers.form_encoded_serializer import FormEncoded
|
||||
from paypalhttp.serializers.json_serializer import Json
|
||||
from paypalhttp.serializers.text_serializer import Text
|
||||
from paypalhttp.serializers.multipart_serializer import Multipart
|
||||
from paypalhttp.serializers.form_part import FormPart
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
try:
|
||||
from urllib import quote
|
||||
except ImportError:
|
||||
from urllib.parse import quote
|
||||
|
||||
class FormEncoded:
|
||||
def encode(self, request):
|
||||
params = []
|
||||
for k, v in request.body.items():
|
||||
params.append("{0}={1}".format(k, quote(v)))
|
||||
|
||||
return '&'.join(params)
|
||||
|
||||
def decode(self, data):
|
||||
raise IOError("FormEncoded does not support deserialization")
|
||||
|
||||
def content_type(self):
|
||||
return "application/x-www-form-urlencoded"
|
||||
@@ -0,0 +1,8 @@
|
||||
class FormPart(object):
|
||||
|
||||
def __init__(self, value, headers):
|
||||
self.value = value
|
||||
self.headers = {}
|
||||
|
||||
for key in headers:
|
||||
self.headers['-'.join(map(lambda word: word[0].upper() + word[1:], key.lower().split('-')))] = headers[key]
|
||||
@@ -0,0 +1,13 @@
|
||||
import json
|
||||
|
||||
|
||||
class Json:
|
||||
|
||||
def encode(self, request):
|
||||
return json.dumps(request.body)
|
||||
|
||||
def decode(self, data):
|
||||
return json.loads(data)
|
||||
|
||||
def content_type(self):
|
||||
return "application/json"
|
||||
@@ -0,0 +1,85 @@
|
||||
import time
|
||||
import os
|
||||
|
||||
from paypalhttp import File
|
||||
from paypalhttp.encoder import Encoder
|
||||
from paypalhttp.serializers.form_part import FormPart
|
||||
|
||||
from paypalhttp.serializers import Json, Text, FormEncoded
|
||||
|
||||
CRLF = "\r\n"
|
||||
|
||||
class FormPartRequest:
|
||||
pass
|
||||
|
||||
class Multipart:
|
||||
|
||||
def encode(self, request):
|
||||
boundary = str(time.time()).replace(".", "")
|
||||
request.headers["content-type"] = "multipart/form-data; boundary=" + boundary
|
||||
params = []
|
||||
form_params = []
|
||||
file_params = []
|
||||
for k, v in request.body.items():
|
||||
if isinstance(v, File):
|
||||
file_params.append(self.add_file_part(k, v))
|
||||
elif isinstance(v, FormPart):
|
||||
form_params.append(self.add_form_part(k, v))
|
||||
else: # It's a regular form param
|
||||
form_params.append(self.add_form_field(k, v))
|
||||
|
||||
params = form_params + file_params
|
||||
data = "--" + boundary + CRLF + ("--" + boundary + CRLF).join(params) + CRLF + "--" + boundary + "--"
|
||||
|
||||
return data
|
||||
|
||||
def decode(self, data):
|
||||
raise IOError('Multipart does not support deserialization.')
|
||||
|
||||
def content_type(self):
|
||||
return "multipart/.*"
|
||||
|
||||
def add_form_field(self, key, value):
|
||||
return "Content-Disposition: form-data; name=\"{}\"{}{}{}{}".format(key, CRLF, CRLF, value, CRLF)
|
||||
|
||||
def add_form_part(self, key, formPart):
|
||||
retValue = "Content-Disposition: form-data; name=\"{}\"".format(key)
|
||||
formatted_headers = self.format_headers(formPart.headers)
|
||||
if formatted_headers["content-type"] == "application/json":
|
||||
retValue += "; filename=\"{}.json\"".format(key)
|
||||
retValue += CRLF
|
||||
|
||||
for key in formPart.headers:
|
||||
retValue += "{}: {}{}".format(key, formPart.headers[key], CRLF)
|
||||
|
||||
retValue += CRLF
|
||||
|
||||
req = FormPartRequest()
|
||||
req.headers = formatted_headers
|
||||
req.body = formPart.value
|
||||
retValue += Encoder([Json(), Text(), FormEncoded()]).serialize_request(req)
|
||||
|
||||
retValue += CRLF
|
||||
return retValue
|
||||
|
||||
def add_file_part(self, key, f):
|
||||
mime_type = self.mime_type_for_filename(os.path.basename(f.name))
|
||||
s = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"{}".format(key, os.path.basename(f.name), CRLF)
|
||||
return s + "Content-Type: {}{}{}{}{}".format(mime_type, CRLF, CRLF, f.read(), CRLF)
|
||||
|
||||
def format_headers(self, headers):
|
||||
if headers:
|
||||
return dict((k.lower(), v) for k, v in headers.items())
|
||||
|
||||
def mime_type_for_filename(self, filename):
|
||||
_, extension = os.path.splitext(filename)
|
||||
if extension == ".jpeg" or extension == ".jpg":
|
||||
return "image/jpeg"
|
||||
elif extension == ".png":
|
||||
return "image/png"
|
||||
elif extension == ".gif":
|
||||
return "image/gif"
|
||||
elif extension == ".pdf":
|
||||
return "application/pdf"
|
||||
else:
|
||||
return "application/octet-stream"
|
||||
@@ -0,0 +1,10 @@
|
||||
class Text:
|
||||
|
||||
def encode(self, request):
|
||||
return str(request.body)
|
||||
|
||||
def decode(self, data):
|
||||
return str(data)
|
||||
|
||||
def content_type(self):
|
||||
return "text/.*"
|
||||
@@ -0,0 +1 @@
|
||||
from paypalhttp.testutils.testharness import TestHarness
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
import responses
|
||||
import unittest
|
||||
import json
|
||||
import paypalhttp
|
||||
|
||||
class TestHarness(unittest.TestCase):
|
||||
|
||||
def environment(self):
|
||||
return paypalhttp.Environment("http://localhost")
|
||||
|
||||
def stub_request_with_empty_reponse(self, request):
|
||||
self.stub_request_with_response(request)
|
||||
|
||||
|
||||
def stub_request_with_response(self, request, response_body="", status=200, content_type="application/json"):
|
||||
body = None
|
||||
if response_body:
|
||||
if isinstance(response_body, str):
|
||||
body = response_body
|
||||
else:
|
||||
body = json.dumps(response_body)
|
||||
|
||||
|
||||
responses.add(request.verb, self.environment().base_url + request.path, body=body, content_type=content_type, status=status)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user