Extending itty3
¶
itty3
is designed to be streamlined & simple to use out of the box.
However, it’s relatively easy to customize as well.
Custom HttpResponse
¶
In normal usage, app.render(...)
is simply a shortcut/convenience
method for constructing an itty3.HttpResponse
object. There’s
nothing extraordinary about these instances, so you can make them yourself
if you prefer:
import pyyaml
@app.get("/whatev/")
def whatev(request):
data = {
"greeting": "Hello",
"noun": "world",
"punctuation": "!",
}
return itty3.HttpResponse(
body=pyyaml.dump(data)
content_type="text/yaml"
)
Or even subclass it to “bake in” complex behavior:
class YAMLResponse(itty3.HttpResponse):
def __init__(self, data, **kwargs):
body = pyyaml.dump(data)
kwargs["content_type"] = "text/yaml"
super().__init__(body, **kwargs)
@app.get("/whatev/")
def whatev(request):
data = {
"greeting": "Hello",
"noun": "world",
"punctuation": "!",
}
return YAMLResponse(data)
And you can override/extend App.render
if you want to automatically
use your new subclass:
class MyApp(itty3.App):
def render(self, request, *args, **kwargs):
# Because of the mismatch in signatures (`body` vs `data`), this
# may not work perfectly in all situations.
return YAMLResponse(*args, **kwargs)
Custom HttpRequest
¶
As with HttpResponse
, itty3.HttpRequest
isn’t particularly
special. The only interesting/tricky part is automatically constructed by
App.create_request
, so you’ll need to override that method when creating
a subclass.
For instance, if you wanted to identify a request was secure in a different way (e.g. verifying a nonce was present in headers):
class NonceHttpRequest(itty3.HttpRequest):
def is_secure(self):
if "X-Secure-Nonce" not in self.headers:
return False
nonce = self.headers["X-Secure-Nonce"]
# Check a DB for that nonce within a time range.
if not verified:
return False
# The request is good. Carry on.
return True
You could then tell your App
to always use this subclass with:
class SuperSecureApp(itty3.App):
def create_request(self, environ):
return NonceHttpRequest.from_wsgi(environ)
Different Routing¶
If itty3
’s routing doesn’t suit you, you can even define your own
variant of Route
. As long as it conforms to a bit of expected API
& with a couple minor tweaks to App
…:
# We want to support routes like:
#
# "/app/:id/:title"
#
# ...and attempt automatic conversion of types.
# This is a naive implementation of that.
class SimpleRoute(object):
def __init__(self, method, path, func):
self.method = method
self.path = path
self.func = func
def split_uri(self, path):
return path.split("/")
def can_handle(self, method, path):
if not self.method == method:
return False
internal_path_bits = self.split_uri(self.path)
external_path_bits = self.split_uri(path)
if len(internal_path_bits) != len(external_path_bits):
# Without even iterating, we know it's not right.
return False
matched = True
for offset, bit in enumerate(internal_path_bits):
if not bit.startswith(":"):
# We're looking for a non-variable, exact match.
if bit != external_path_bits[offset]:
matched = False
break
else:
# It's a variable. Carry on.
continue
return matched
def extract_kwargs(self, path):
# This only gets called if the route can handle the URI.
# So we'll take a shortcut or two here for brevity.
internal_path_bits = self.split_uri(self.path)
external_path_bits = self.split_uri(path)
matches = {}
for offset, bit in enumerate(internal_path_bits):
if not bit.startswith(":"):
# It's not a variable, we don't care.
continue
# It's a variable. Slice off the colon.
var_name = bit[1:]
# Extract it from the actual URI.
value = external_path_bits[offset]
# Try to convert the type.
try:
value = int(value)
except ValueError:
pass
try:
value = float(value)
except ValueError:
pass
if value in ("true", "false"):
# Store it as a boolean.
value = value == "true"
# Finally, track what we found.
matches[var_name] = value
return matches
# Now, just override ``App`` to use your new routes.
class SimpleRoutesApp(itty3.App):
def add_route(self, method, path, func):
# We swap in the custom class here.
route = SimpleRoute(method, path, func)
self._routes.append(route)