Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(user): add query strings tutorial #2239

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 210 additions & 4 deletions docs/user/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ Go ahead and edit your ``images.py`` file to look something like this:

_CHUNK_SIZE_BYTES = 4096
_IMAGE_NAME_PATTERN = re.compile(
'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z]{2,4}$'
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z]{2,4}$'
)

def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open):
Expand Down Expand Up @@ -1304,10 +1304,216 @@ Inspecting the application now returns:
⇒ /images/{name} - Item:
└── GET - on_get

.. Query Strings
.. -------------
Query Strings
-------------
Now that we are able to get the images from the service, we need a way to get
a list of available images. We have already set up this route. Before testing this
route let's change its output format back to JSON to have a more
terminal-friendly output. The top of file ``images.py`` should look like this:

.. code:: python

import io
import os
import re
import uuid
import mimetypes

import falcon
import json


class Collection:

def __init__(self, image_store):
self._image_store = image_store

def on_get(self, req, resp):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version should be a static one like the above that serializes to msgpack, since we refer to it as "static"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad 🥲 . You're totally right.

# TODO: Modify this to return a list of href's based on
# what images are actually available.
doc = {
'images': [
{
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png'
}
]
}

resp.text = json.dumps(doc, ensure_ascii=False)
resp.status = falcon.HTTP_200

def on_post(self, req, resp):
name = self._image_store.save(req.stream, req.content_type)
resp.status = falcon.HTTP_201
resp.location = '/images/' + name


Now try the following:

.. code:: bash

http localhost:8000/images

In response you should get the following data that we statically have put in the code.

.. code::

{
"images": [
{
"href": "/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png"
}
]
}

Let's go back to the ``on_get`` method and create a dynamic response. We can
use query strings to set maximum image size and get the list of all images
smaller than the specified value. We will use method ``get_param_as_int`` to
set a default value of ``-1`` in case no ``maxsize`` query string was provided
and also to enable a minimum value validation.

.. code:: python
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to update any example files on disk? And write tests for them too?

(See also: #2247.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll look into this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vytas7, I see that a sample usage of query params is being used in the example file for wsgi.

limit = req.get_param_as_int('limit') or 50

Are we looking for a more descriptive example?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi again, no, sorry I was not very clear!

No, it has nothing to do with things_advanced.py; what I meant was that we are tracking tutorial files in the Git tree as well, we even have some rudimentary tests for them. In this case it looks like you might need to update images.py with your additions, and potentially even revise how the file develops further in this tutorial.


import io
import os
import re
import uuid
import mimetypes

import falcon
import json


class Collection:

def __init__(self, image_store):
self._image_store = image_store

def on_get(self, req, resp):
max_size = req.get_param_as_int("maxsize", min_value=1, default=-1)
images = self._image_store.list(max_size)
doc = {
'images': [
{'href': '/images/' + image} for image in images
]
}

resp.text = json.dumps(doc, ensure_ascii=False)
resp.status = falcon.HTTP_200

def on_post(self, req, resp):
name = self._image_store.save(req.stream, req.content_type)
resp.status = falcon.HTTP_201
resp.location = '/images/' + name


class Item:

def __init__(self, image_store):
self._image_store = image_store

def on_get(self, req, resp, name):
resp.content_type = mimetypes.guess_type(name)[0]
resp.stream, resp.content_length = self._image_store.open(name)


class ImageStore:

_CHUNK_SIZE_BYTES = 4096
_IMAGE_NAME_PATTERN = re.compile(
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z]{2,4}$'
)

def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open):
self._storage_path = storage_path
self._uuidgen = uuidgen
self._fopen = fopen

def save(self, image_stream, image_content_type):
ext = mimetypes.guess_extension(image_content_type)
name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext)
image_path = os.path.join(self._storage_path, name)

with self._fopen(image_path, 'wb') as image_file:
while True:
chunk = image_stream.read(self._CHUNK_SIZE_BYTES)
if not chunk:
break

image_file.write(chunk)

return name

def open(self, name):
# Always validate untrusted input!
if not self._IMAGE_NAME_PATTERN.match(name):
raise IOError('File not found')

image_path = os.path.join(self._storage_path, name)
stream = self._fopen(image_path, 'rb')
content_length = os.path.getsize(image_path)

return stream, content_length

def list(self, max_size):
images = [
image for image in os.listdir(self._storage_path)
if self._IMAGE_NAME_PATTERN.match(image)
and (
max_size == -1
or os.path.getsize(os.path.join(self._storage_path, image)) <= max_size
)
]
return images

As you can see the method ``list`` has been added to ``ImageStore`` in order
to return list of available images smaller than ``max_size`` unless it is not
``-1``, in which case it will behave like there was no predicament of image size.
Let's try to save some binary data as images in the service and then try to
retrieve their list. Execute the following commands in order to simulate the
creation of 3 files as images with different sizes. While these are not valid
PNG files, they will work for this tutorial.

.. code:: bash

echo "First Case" > pseudo-image-1.png
echo "Second Case" > pseudo-image-2.png
echo "3rd Case" > pseudo-image-3.png

Now we need to store these files using ``POST`` request:

.. code:: bash

http POST localhost:8000/images Content-Type:image/png < pseudo-image-1.png
http POST localhost:8000/images Content-Type:image/png < pseudo-image-2.png
http POST localhost:8000/images Content-Type:image/png < pseudo-image-3.png

If we check the size of these files, we will see that they are 11, 12, 9 bytes
respectively. Let's try to get the list of the images which are smaller or
equal to 11 bytes.

.. code:: bash

http localhost:8000/images?maxsize=11

We expect to get a list of 2 files, which will be similar to the following:

.. code::

{
"images": [
{
"href": "/images/7ba2ebc9-726f-46b0-9615-a69824f5089b.png"
},
{
"href": "/images/e4354a31-2161-4064-805c-3bc7c332e7e6.png"
}
]
}

You could also now validate the response with getting the image files using
the ``href`` value in the response and compare them with the original files.

.. *Coming soon...*

Introducing Hooks
-----------------
Expand Down
7 changes: 4 additions & 3 deletions examples/look/look/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import falcon

from .images import Collection
from .images import ImageStore
from .images import Resource
from .images import Item


def create_app(image_store):
image_resource = Resource(image_store)
app = falcon.App()
app.add_route('/images', image_resource)
app.add_route('/images', Collection(image_store))
app.add_route('/images/{name}', Item(image_store))
return app


Expand Down
56 changes: 43 additions & 13 deletions examples/look/look/images.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import io
import json
import mimetypes
import os
import re
import uuid

import msgpack

import falcon


class Resource:
class Collection:
def __init__(self, image_store):
self._image_store = image_store

def on_get(self, req, resp):
doc = {
'images': [
{
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png',
},
],
}

resp.data = msgpack.packb(doc, use_bin_type=True)
resp.content_type = 'application/msgpack'
max_size = req.get_param_as_int('maxsize', min_value=1, default=-1)
images = self._image_store.list(max_size)
doc = {'images': [{'href': '/images/' + image} for image in images]}

resp.text = json.dumps(doc, ensure_ascii=False)
CaselIT marked this conversation as resolved.
Show resolved Hide resolved
resp.status = falcon.HTTP_200

def on_post(self, req, resp):
Expand All @@ -31,8 +26,20 @@ def on_post(self, req, resp):
resp.location = '/images/' + name


class Item:
def __init__(self, image_store):
self._image_store = image_store

def on_get(self, req, resp, name):
resp.content_type = mimetypes.guess_type(name)[0]
resp.stream, resp.content_length = self._image_store.open(name)


class ImageStore:
_CHUNK_SIZE_BYTES = 4096
_IMAGE_NAME_PATTERN = re.compile(
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z]{2,4}$'
)

# Note the use of dependency injection for standard library
# methods. We'll use these later to avoid monkey-patching.
Expand All @@ -55,3 +62,26 @@ def save(self, image_stream, image_content_type):
image_file.write(chunk)

return name

def open(self, name):
# Always validate untrusted input!
if not self._IMAGE_NAME_PATTERN.match(name):
raise IOError('File not found')

image_path = os.path.join(self._storage_path, name)
stream = self._fopen(image_path, 'rb')
content_length = os.path.getsize(image_path)

return stream, content_length

def list(self, max_size):
images = [
image
for image in os.listdir(self._storage_path)
if self._IMAGE_NAME_PATTERN.match(image)
and (
max_size == -1
or os.path.getsize(os.path.join(self._storage_path, image)) <= max_size
)
]
return images
Loading