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

Merged
merged 18 commits into from
Nov 7, 2024
Merged
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
195 changes: 192 additions & 3 deletions docs/user/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1295,10 +1295,199 @@ 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 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:
vytas7 marked this conversation as resolved.
Show resolved Hide resolved

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

max_size = int(req.params.get("maxsize", 0))
images = self._image_store.list(max_size)
doc = {
'images': [
{'href': '/images/'+image} for image in images
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{'href': '/images/'+image} for image in 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


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.

.. 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 = int(req.params.get("maxsize", 0))
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be useful to showcase get_param_as_int here, so we could do

Suggested change
max_size = int(req.params.get("maxsize", 0))
max_size = int(req.get_param_as_int("maxsize", default=0, min_value=1))

(I've added a min value but we may omit it too)

images = self._image_store.list(max_size)
doc = {
'images': [
{'href': '/images/'+image} for image in images
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{'href': '/images/'+image} for image in 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(
'[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=0):
images = [
image for image in os.listdir(self._storage_path)
if self._IMAGE_NAME_PATTERN.match(image)
and (
max_size == 0
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 zero, in which case it will behave like there was no predicament of image size.
Copy link
Member

Choose a reason for hiding this comment

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

maybe we could use -1 for the "defaut" here?

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 make 3 files as images with different sizes.
Copy link
Member

Choose a reason for hiding this comment

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

since these are not really images, we could use "images" here and maybe say something like

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" > 1.png
echo "Second Case" > 2.png
echo "3rd Case" > 3.png

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

.. code:: bash

http POST localhost:8000/images Content-Type:image/png < 1.png
http POST localhost:8000/images Content-Type:image/png < 2.png
http POST localhost:8000/images Content-Type:image/png < 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