This commit is contained in:
Iliyan Angelov
2025-11-19 12:27:01 +02:00
parent 2043ac897c
commit 34b4c969d4
469 changed files with 26870 additions and 8329 deletions

View File

@@ -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 *

View File

@@ -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

View File

@@ -0,0 +1,4 @@
class Environment(object):
def __init__(self, base_url):
self.base_url = base_url

View 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')

View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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]

View File

@@ -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"

View File

@@ -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"

View File

@@ -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/.*"

View File

@@ -0,0 +1 @@
from paypalhttp.testutils.testharness import TestHarness

View File

@@ -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)