Skip to content

Commit

Permalink
update examples/look and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bssyousefi committed Sep 1, 2024
1 parent ae44d2b commit 636ac6c
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 27 deletions.
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
8 changes: 4 additions & 4 deletions examples/look/look/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import falcon

from .images import ImageStore
from .images import Resource
from .images import Collection, ImageStore, 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


def get_app():
storage_path = os.environ.get('LOOK_STORAGE_PATH', '.')
image_store = ImageStore(storage_path)
return create_app(image_store)

52 changes: 44 additions & 8 deletions examples/look/look/images.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import io
import mimetypes
import os
import re
import uuid

import msgpack
import json

import falcon


class Resource:
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/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png',
},
],
{'href': '/images/' + image} for image in images
]
}

resp.data = msgpack.packb(doc, use_bin_type=True)
resp.content_type = 'application/msgpack'
resp.text = json.dumps(doc, ensure_ascii=False)
resp.status = falcon.HTTP_200

def on_post(self, req, resp):
Expand All @@ -31,8 +31,21 @@ 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 +68,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: 66 additions & 11 deletions examples/look/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import io
import json
import os
import uuid
from unittest import TestCase
from unittest.mock import call
from unittest.mock import MagicMock
from unittest.mock import mock_open
Expand All @@ -25,19 +29,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)

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


Expand All @@ -64,7 +66,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 +86,56 @@ 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:
file_length = 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)

0 comments on commit 636ac6c

Please sign in to comment.