Skip to content

Commit

Permalink
Adds TestDataServer to serve testdata calls over the web. I'm working…
Browse files Browse the repository at this point in the history
… on a few frontend things and I wanted to have access to testdata methods to generate random data. This is the first step for issue #33
  • Loading branch information
Jaymon committed Feb 8, 2024
1 parent 253cf3a commit 9e1d2f5
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 33 deletions.
192 changes: 164 additions & 28 deletions testdata/server.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, division, print_function, absolute_import
import logging
import re

from datatypes.url import Host
from datatypes.url import Host, Url
from datatypes.server import (
ServerThread,
PathServer,
CallbackServer,
MethodServer,
)
from datatypes.utils import infer_type

from .compat import *
from .config import environ
Expand All @@ -22,8 +24,9 @@
###############################################################################

class Server(ServerThread):
"""This is the Webserver master class, it masquerades as a string whose value
is the url scheme://hostname:port but adds helper methods to manage the webserver
"""This is the Webserver master class, it masquerades as a string whose
value is the url scheme://hostname:port but adds helper methods to manage
the webserver
:Example:
s = Server(PathServer("<SOME-PATH>"))
Expand All @@ -48,7 +51,8 @@ def url(self, *parts, **kwargs):
:param *parts: list, the path parts you will add to the scheme://netloc
:returns: the full url scheme://netloc/parts
"""
# DEPRECATED? this class extends Url so you can use all Url's helper methods
# DEPRECATED? this class extends Url so you can use all Url's helper
# methods
return self.child(*parts, **kwargs)


Expand All @@ -68,24 +72,12 @@ def make_morsels(self, handler):
ret = []
for name, val in handler.server.cookies:
c = SimpleCookie()
if is_py2:
if isinstance(val, Mapping):
# TODO: this isn't right :(
c[bytes(name)] = val
else:
# NOTE -- cookies can't really have unicode values
if isinstance(val, basestring):
val = val.encode("utf-8")
else:
val = bytes(val)
c[name.encode("utf-8")] = val
if isinstance(val, Mapping):
# TODO: this isn't right :(
c[name] = val

else:
if isinstance(val, Mapping):
# TODO: this isn't right :(
c[name] = val
else:
c[str(name)] = str(val)
c[str(name)] = str(val)

ret.extend(c.values())
return ret
Expand All @@ -106,7 +98,9 @@ def callback(self, handler):
read_cookies = {}
unread_cookies = {}

server_morsels = set(m.OutputString() for m in self.make_morsels(handler))
server_morsels = set(
m.OutputString() for m in self.make_morsels(handler)
)
total_server_morsels = len(server_morsels)
if is_py2:
req_c = SimpleCookie(b"\r\n".join(req_cookies.split(b", ")))
Expand All @@ -128,8 +122,9 @@ def callback(self, handler):
ret["unread_cookies"] = unread_cookies

else:
# Turns out Chrome won't set a cookie on a 204, this might be a thing
# in the spec, but just to be safe we will send information down
# Turns out Chrome won't set a cookie on a 204, this might be a
# thing in the spec, but just to be safe we will send information
# down
handler.send_response(200)
count = 0
sent_cookies = {}
Expand All @@ -155,11 +150,137 @@ def __init__(self, cookies, *args, **kwargs):
super().__init__({"default": self.callback}, *args, **kwargs)


class TestDataServer(MethodServer):
"""Create a testdata server that basically turns all TestData children
methods into a json api
The server will answer requests in the form of:
<HOST>/<METHOD-NAME>/<ARGS>?<KWARGS>
:Example:
s = TestDataServer(("localhost", 4321)
testdata.fetch("http://localhost:4321/get_int/1/100")
"""
def get_method_call(self, handler):
"""From the path, query, and body figure out the method name and the
arguments that will be passed to it
:param handler: CallbackHandler, the handler that is handling the
request
:returns: tuple[str, list, dict]
"""
kwargs = handler.query
if body := handler.body:
kwargs.update(body)
kwargs = infer_type(kwargs)

parts = Url(handler.path).parts
if len(parts) > 1:
method_name = parts[0]
args = infer_type(parts[1:])

else:
method_name = parts[0]
args = []

return method_name, args, kwargs

def get_object_json(self, o):
"""If the testdata method that was ran returns an object then this will
try and figure out how to turn that object into json
:param o: object, the generic object whose json value couldn't be
inferred
:returns: dict
"""
# https://stackoverflow.com/a/51055044
if hasattr(o, "jsonable"):
ret = o.jsonable()

elif hasattr(cbr, "to_json"):
ret = cbr.to_json()

elif hasattr(cbr, "toJSON"):
ret = cbr.toJSON()

elif hasattr(cbr, "tojson"):
ret = cbr.tojson()

elif hasattr(cbr, "json"):
ret = cbr.json()

elif hasattr(cbr, "__json__"):
ret = cbr.__json__()

else:
raise ValueError(f"No idea how to json encode {type(o)} object")

return ret

def run_method(self, handler):
"""Internal method to figure out and run the testdata method that was
requested
:param handler: CallbackHandler, the handler that is handling the
request
:returns: Any, it will return whatever the ran method returned
"""
method_name, args, kwargs = self.get_method_call(handler)
cb = TestData.__findattr__(method_name)
if callable(cb):
cbr = cb(*args, **kwargs)

else:
cbr = cb

if cbr is None:
ret = None

if isinstance(cbr, (basestring, float, int, bool)):
ret = cbr

elif isinstance(cbr, Mapping):
ret = cbr

elif isinstance(cbr, Sequence):
ret = []
for o in cbr:
try:
ret.append(self.get_object_json(o))

except ValueError:
ret.append(o)

elif isinstance(cbr, object):
ret = self.get_object_json(cbr)

else:
ret = cbr

return ret

def GET(self, handler):
"""Answer GET requests"""
return self.run_method(handler)

def POST(self, handler):
"""Answer POST requests"""
return self.run_method(handler)


###############################################################################
# testdata functions
###############################################################################
class ServerData(TestData):
def create_fileserver(self, file_dict, tmpdir="", hostname="", port=0, encoding=""):
def create_fileserver(
self,
file_dict,
tmpdir="",
hostname="",
port=0,
encoding=""
):
"""create a fileserver that can be used to test remote file retrieval
:Example:
Expand All @@ -179,7 +300,13 @@ def create_fileserver(self, file_dict, tmpdir="", hostname="", port=0, encoding=
}

path = self.create_files(file_dict, tmpdir=tmpdir, encoding=encoding)
return Server(PathServer(path, server_address=(hostname, port), encoding=encoding))
return Server(
PathServer(
path,
server_address=(hostname, port),
encoding=encoding
)
)
create_file_server = create_fileserver
create_pathserver = create_fileserver
create_path_server = create_fileserver
Expand Down Expand Up @@ -215,8 +342,8 @@ def do_PUT(handler):
https://github.com/Jaymon/testdata/issues/79
:param cb_dict: dict, key is the http method and value is the callback, the
callback should have a signature of (handler)
:param cb_dict: dict, key is the http method and value is the callback,
the callback should have a signature of (handler)
:param hostname: str, usually leave this alone and it will use localhost
:param port: int, the port you want to use
"""
Expand All @@ -226,3 +353,12 @@ def do_PUT(handler):
create_cbserver = create_callbackserver
get_callbackserver = create_callbackserver

def serve(self, hostname="", port=0, server_class=TestDataServer):
"""A wrapper around the TestDataServer class's .server_forever method
:param hostname: str, usually leave this alone and it will use localhost
:param port: int, the port you want to use
"""
s = server_class(server_address=(hostname, port))
s.serve_forever()

10 changes: 8 additions & 2 deletions testdata/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,15 @@ def __str__(self):
###############################################################################
class UserData(TestData):
def get_username(self, name=""):
"""Returns just a non-space ascii name, this is a very basic username generator"""
"""Returns just a non-space ascii name, this is a very basic username
generator"""
if not name:
name = self.get_ascii_first_name() if self.yes() else self.get_ascii_last_name()
if self.yes():
name = self.get_ascii_first_name()

else:
name = self.get_ascii_last_name()

name = re.sub(r"['-]", "", name)
return name

Expand Down
30 changes: 27 additions & 3 deletions tests/server_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, division, print_function, absolute_import

from testdata.config import environ
from testdata.compat import *
from testdata.config import environ
from testdata.server import TestDataServer, Server

from . import TestCase, testdata

Expand Down Expand Up @@ -92,5 +92,29 @@ def test_cookies(self):

# test with different case
res = testdata.fetch(server, headers={"cookie": "foo=1234"})
self.assertEqual("1234", res.json()["unread_cookies"]["foo"]["value"])
self.assertEqual(
"1234",
res.json()["unread_cookies"]["foo"]["value"]
)


class TestDataServerTest(TestCase):
def test_request(self):
s = Server(TestDataServer())
with s:
res = self.fetch(
s.child("get_int"),
query={"min_size": 1, "max_size": 10}
).json()
self.assertTrue(1 <= res <= 10)

#res = self.fetch(s, query={"foo": 1, "bar": "two"})
res = self.fetch(s.child("get_int/1/10")).json()
self.assertTrue(1 <= res <= 10)

res = self.fetch(
s.child("get_int"),
body={"min_size": 1, "max_size": 10}
).json()
self.assertTrue(1 <= res <= 10)

0 comments on commit 9e1d2f5

Please sign in to comment.