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
Show file tree
Hide file tree
Changes from 4 commits
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
8 changes: 4 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 @@ -1307,7 +1307,7 @@ Inspecting the application now returns:
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
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:

Expand Down Expand Up @@ -1390,7 +1390,7 @@ and also to enable a minimum value validation.
self._image_store = image_store

def on_get(self, req, resp):
max_size = req.get_param_as_int("maxsize", min_value=1, default=-1))
max_size = req.get_param_as_int("maxsize", min_value=1, default=-1)
images = self._image_store.list(max_size)
doc = {
'images': [
Expand Down Expand Up @@ -1421,7 +1421,7 @@ and also to enable a minimum value validation.

_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
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
77 changes: 65 additions & 12 deletions examples/look/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import io
import json
import os
from unittest import TestCase
from unittest.mock import call
from unittest.mock import MagicMock
from unittest.mock import mock_open
import uuid
from wsgiref.validate import InputWrapper

import msgpack
import pytest

import falcon
Expand All @@ -25,19 +28,17 @@ def client(mock_store):
return testing.TestClient(api)


def test_list_images(client):
doc = {
'images': [
{
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png',
},
],
}
def test_list_images(client, mock_store):
images = ['first-file', 'second-file', 'third-file']
image_docs = [{'href': '/images/' + image} for image in images]

mock_store.list.return_value = images

response = client.simulate_get('/images')
result_doc = msgpack.unpackb(response.content, raw=False)

assert result_doc == doc
result = json.loads(response.content)
CaselIT marked this conversation as resolved.
Show resolved Hide resolved

assert result['images'] == image_docs
assert response.status == falcon.HTTP_OK


Expand All @@ -64,7 +65,7 @@ def test_post_image(client, mock_store):
assert saver_call[0][1] == image_content_type


def test_saving_image(monkeypatch):
def test_saving_image():
# This still has some mocks, but they are more localized and do not
# have to be monkey-patched into standard library modules (always a
# risky business).
Expand All @@ -84,3 +85,55 @@ def mock_uuidgen():

assert store.save(fake_request_stream, 'image/png') == fake_uuid + '.png'
assert call().write(fake_image_bytes) in mock_file_open.mock_calls


def test_get_image(client, mock_store):
file_bytes = b'fake-image-bytes'

mock_store.open.return_value = ((file_bytes,), 17)

response = client.simulate_get('/images/filename.png')

assert response.status == falcon.HTTP_OK
assert response.content == file_bytes


def test_opening_image():
file_name = f'{uuid.uuid4()}.png'
storage_path = '.'
file_path = f'{storage_path}/{file_name}'
fake_image_bytes = b'fake-image-bytes'
with open(file_path, 'wb') as image_file:
file_length = image_file.write(fake_image_bytes)

store = look.images.ImageStore(storage_path)

file_reader, content_length = store.open(file_name)
assert content_length == file_length
assert file_reader.read() == fake_image_bytes
os.remove(file_path)

with TestCase().assertRaises(IOError):
store.open('wrong_file_name_format')


def test_listing_images():
file_names = [f'{uuid.uuid4()}.png' for _ in range(2)]
storage_path = '.'
file_paths = [f'{storage_path}/{name}' for name in file_names]
fake_images_bytes = [
b'fake-image-bytes', # 17
b'fake-image-bytes-with-more-length', # 34
]
for i in range(2):
with open(file_paths[i], 'wb') as image_file:
image_file.write(fake_images_bytes[i])

store = look.images.ImageStore(storage_path)
assert store.list(10) == []
assert store.list(20) == [file_names[0]]
assert len(store.list(40)) == 2
assert sorted(store.list(40)) == sorted(file_names)

for file_path in file_paths:
os.remove(file_path)