├── test ├── __init__.py ├── data │ ├── bar │ ├── foo.txt │ └── foo.c ├── __main__.py ├── data.py ├── test_multi_dict.py ├── test_cube.py ├── test_regex_route.py ├── test_response.py ├── test_request.py ├── test_wildcard.py ├── test_router.py ├── test_ice.py ├── test_wildcard_route.py └── test_examples.py ├── MANIFEST.in ├── .gitignore ├── docs ├── ice.rst ├── index.rst ├── conf.py ├── Makefile ├── make.bat └── tutorial.rst ├── .travis.yml ├── CHANGES.rst ├── Makefile ├── LICENSE.rst ├── setup.py ├── README.rst └── ice.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/bar: -------------------------------------------------------------------------------- 1 |
bar
2 | -------------------------------------------------------------------------------- /test/data/foo.txt: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /test/data/foo.c: -------------------------------------------------------------------------------- 1 | #includeThis is the default ice web page.
\n' 57 | b'\n' 58 | b'\n') 59 | 60 | def test_error_page(self): 61 | self.run_app() 62 | with self.assertRaises(urllib.error.HTTPError) as cm: 63 | urllib.request.urlopen('http://127.0.0.1:8080/foo') 64 | self.assertEqual(cm.exception.code, 404) 65 | self.assertEqual(cm.exception.read(), 66 | b'\n' 67 | b'\n' 68 | b'Nothing matches the given URI
\n' 72 | b'Foo
' 62 | app = ice.Ice() 63 | @app.get('/') 64 | def foo(): 65 | return expected 66 | 67 | m = unittest.mock.Mock() 68 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 69 | m.assert_called_with('200 OK', [ 70 | ('Content-Type', 'text/html; charset=UTF-8'), 71 | ('Content-Length', str(len(expected))) 72 | ]) 73 | self.assertEqual(r, [expected.encode()]) 74 | 75 | expected = '501 Not Implemented' 76 | r = app({'REQUEST_METHOD': 'POST', 'PATH_INFO': '/'}, m) 77 | m.assert_called_with(expected, [ 78 | ('Content-Type', 'text/plain; charset=UTF-8'), 79 | ('Content-Length', str(len(expected))) 80 | ]) 81 | self.assertEqual(r, [expected.encode()]) 82 | 83 | def test_post_route(self): 84 | expected = 'Foo
' 85 | app = ice.Ice() 86 | @app.post('/') 87 | def foo(): 88 | return expected 89 | 90 | m = unittest.mock.Mock() 91 | r = app({'REQUEST_METHOD': 'POST', 'PATH_INFO': '/'}, m) 92 | m.assert_called_with('200 OK', [ 93 | ('Content-Type', 'text/html; charset=UTF-8'), 94 | ('Content-Length', str(len(expected))) 95 | ]) 96 | self.assertEqual(r, [expected.encode()]) 97 | 98 | expected = '501 Not Implemented' 99 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 100 | m.assert_called_with(expected, [ 101 | ('Content-Type', 'text/plain; charset=UTF-8'), 102 | ('Content-Length', str(len(expected))) 103 | ]) 104 | self.assertEqual(r, [expected.encode()]) 105 | 106 | def test_error_in_callback(self): 107 | app = ice.Ice() 108 | @app.get('/') 109 | def foo(): 110 | raise NotImplementedError() 111 | 112 | with self.assertRaises(NotImplementedError) as cm: 113 | app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, 114 | unittest.mock.Mock()) 115 | 116 | def test_error_callback(self): 117 | expected = 'HTTP method not implemented
' 118 | app = ice.Ice() 119 | @app.error(501) 120 | def error(): 121 | return expected 122 | m = unittest.mock.Mock() 123 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 124 | m.assert_called_with('501 Not Implemented', [ 125 | ('Content-Type', 'text/html; charset=UTF-8'), 126 | ('Content-Length', str(len(expected))) 127 | ]) 128 | self.assertEqual(r, [expected.encode()]) 129 | 130 | def test_return_code_from_callback(self): 131 | app = ice.Ice() 132 | @app.get('/') 133 | def foo(): 134 | return 200 135 | 136 | @app.get('/bar') 137 | def bar(): 138 | return 404 139 | 140 | expected2 = 'Baz
' 141 | @app.get('/baz') 142 | def baz(): 143 | app.response.body = expected2 144 | return 200 145 | 146 | expected = '200 OK' 147 | m = unittest.mock.Mock() 148 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 149 | m.assert_called_with(expected, [ 150 | ('Content-Type', 'text/plain; charset=UTF-8'), 151 | ('Content-Length', str(len(expected))) 152 | ]) 153 | self.assertEqual(r, [expected.encode()]) 154 | 155 | expected = '404 Not Found' 156 | m = unittest.mock.Mock() 157 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/bar'}, m) 158 | m.assert_called_with(expected, [ 159 | ('Content-Type', 'text/plain; charset=UTF-8'), 160 | ('Content-Length', str(len(expected))) 161 | ]) 162 | self.assertEqual(r, [expected.encode()]) 163 | 164 | m = unittest.mock.Mock() 165 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/baz'}, m) 166 | m.assert_called_with('200 OK', [ 167 | ('Content-Type', 'text/html; charset=UTF-8'), 168 | ('Content-Length', str(len(expected2))) 169 | ]) 170 | self.assertEqual(r, [expected2.encode()]) 171 | 172 | def test_invalid_return_type_from_callback(self): 173 | app = ice.Ice() 174 | @app.get('/') 175 | def foo(): 176 | return [] 177 | 178 | with self.assertRaises(ice.Error) as cm: 179 | app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, 180 | unittest.mock.Mock()) 181 | self.assertEqual(str(cm.exception), 'Route callback for GET / ' 182 | 'returned invalid value: list: []') 183 | 184 | def test_invalid_return_code_from_callback(self): 185 | app = ice.Ice() 186 | @app.get('/') 187 | def foo(): 188 | return 1000 189 | 190 | with self.assertRaises(ice.Error) as cm: 191 | app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, 192 | unittest.mock.Mock()) 193 | self.assertEqual(str(cm.exception), 'Route callback for GET / ' 194 | 'returned invalid value: int: 1000') 195 | 196 | def test_redirect(self): 197 | app = ice.Ice() 198 | 199 | expected = '303 See Other' 200 | 201 | @app.get('/') 202 | def foo(): 203 | return 303, '/foo' 204 | 205 | expected2 = 'Bar
' 206 | @app.get('/bar') 207 | def bar(): 208 | app.response.body = expected2 209 | return 303, '/baz' 210 | 211 | m = unittest.mock.Mock() 212 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 213 | m.assert_called_with(expected, [ 214 | ('Location', '/foo'), 215 | ('Content-Type', 'text/plain; charset=UTF-8'), 216 | ('Content-Length', str(len(expected))) 217 | ]) 218 | self.assertEqual(r, [expected.encode()]) 219 | 220 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/bar'}, m) 221 | m.assert_called_with(expected, [ 222 | ('Location', '/baz'), 223 | ('Content-Type', 'text/html; charset=UTF-8'), 224 | ('Content-Length', str(len(expected2))) 225 | ]) 226 | self.assertEqual(r, [expected2.encode()]) 227 | 228 | def test_run_and_exit(self): 229 | app = ice.Ice() 230 | threading.Thread(target=app.run).start() 231 | while not app.running(): 232 | time.sleep(0.1) 233 | app.exit() 234 | # Calling another unnecessary exit should cause no problem. 235 | app.exit() 236 | 237 | def test_run_exit_without_run(self): 238 | app = ice.Ice() 239 | app.exit() 240 | # Calling another unnecessary exit should cause no problem. 241 | app.exit() 242 | 243 | def test_run_serve_and_exit(self): 244 | app = self.app = ice.Ice() 245 | expected = 'Foo
' 246 | 247 | @app.get('/') 248 | def foo(): 249 | return expected 250 | 251 | @app.get('/bar') 252 | def bar(): 253 | raise NotImplementedError() 254 | 255 | threading.Thread(target=app.run).start() 256 | while not app.running(): 257 | time.sleep(0.1) 258 | 259 | # 200 OK 260 | r = urllib.request.urlopen('http://127.0.0.1:8080/') 261 | self.assertEqual(r.status, 200) 262 | self.assertEqual(r.reason, 'OK') 263 | self.assertEqual(r.getheader('Content-Type'), 264 | 'text/html; charset=UTF-8') 265 | self.assertEqual(r.getheader('Content-Length'), 266 | str(len(expected))) 267 | self.assertEqual(r.read(), expected.encode()) 268 | 269 | # 404 Not Found 270 | expected = '404 Not Found' 271 | with self.assertRaises(urllib.error.HTTPError) as cm: 272 | urllib.request.urlopen('http://127.0.0.1:8080/foo') 273 | self.assertEqual(cm.exception.code, 404) 274 | self.assertEqual(cm.exception.reason, 'Not Found') 275 | h = dict(cm.exception.headers) 276 | self.assertEqual(h['Content-Type'], 'text/plain; charset=UTF-8') 277 | self.assertEqual(h['Content-Length'], str(len(expected))) 278 | self.assertEqual(cm.exception.read(), expected.encode()) 279 | 280 | # 501 Not Implemented 281 | expected = '501 Not Implemented' 282 | with self.assertRaises(urllib.error.HTTPError) as cm: 283 | urllib.request.urlopen('http://127.0.0.1:8080/', b'') 284 | self.assertEqual(cm.exception.code, 501) 285 | self.assertEqual(cm.exception.reason, 'Not Implemented') 286 | h = dict(cm.exception.headers) 287 | self.assertEqual(h['Content-Type'], 'text/plain; charset=UTF-8') 288 | self.assertEqual(h['Content-Length'], str(len(expected))) 289 | self.assertEqual(cm.exception.read(), expected.encode()) 290 | 291 | # Exception while processing request 292 | with self.assertRaises(urllib.error.HTTPError) as cm: 293 | urllib.request.urlopen('http://127.0.0.1:8080/bar') 294 | self.assertEqual(cm.exception.code, 500) 295 | self.assertEqual(cm.exception.reason, 'Internal Server Error') 296 | 297 | def test_static(self): 298 | app = ice.Ice() 299 | 300 | @app.get('/foo') 301 | def foo(): 302 | return app.static(data.dirpath, 'foo.txt') 303 | 304 | @app.get('/bar') 305 | def bar(): 306 | return app.static(data.dirpath, 'bar', 'text/html') 307 | 308 | m = unittest.mock.Mock() 309 | 310 | expected = 'foo\n' 311 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/foo'}, m) 312 | m.assert_called_with('200 OK', [ 313 | ('Content-Type', 'text/plain; charset=UTF-8'), 314 | ('Content-Length', str(len(expected))) 315 | ]) 316 | self.assertEqual(r, [expected.encode()]) 317 | 318 | expected = 'bar
\n' 319 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/bar'}, m) 320 | m.assert_called_with('200 OK', [ 321 | ('Content-Type', 'text/html; charset=UTF-8'), 322 | ('Content-Length', str(len(expected))) 323 | ]) 324 | self.assertEqual(r, [expected.encode()]) 325 | 326 | def test_static_403_error(self): 327 | app = ice.Ice() 328 | 329 | @app.get('/') 330 | def foo(): 331 | return app.static(data.filepath('subdir'), '../foo.txt') 332 | 333 | m = unittest.mock.Mock() 334 | 335 | expected = '403 Forbidden' 336 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 337 | m.assert_called_with(expected, [ 338 | ('Content-Type', 'text/plain; charset=UTF-8'), 339 | ('Content-Length', str(len(expected))) 340 | ]) 341 | self.assertEqual(r, [expected.encode()]) 342 | 343 | def test_static_avoid_403_error(self): 344 | app = ice.Ice() 345 | 346 | @app.get('/') 347 | def foo(): 348 | return app.static('/', data.filepath('foo.txt')) 349 | 350 | m = unittest.mock.Mock() 351 | 352 | expected = 'foo\n' 353 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 354 | m.assert_called_with('200 OK', [ 355 | ('Content-Type', 'text/plain; charset=UTF-8'), 356 | ('Content-Length', str(len(expected))) 357 | ]) 358 | self.assertEqual(r, [expected.encode()]) 359 | 360 | def test_static_404_error(self): 361 | app = ice.Ice() 362 | 363 | @app.get('/') 364 | def foo(): 365 | return app.static(data.dirpath, 'nonexistent.txt') 366 | 367 | m = unittest.mock.Mock() 368 | 369 | expected = '404 Not Found' 370 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 371 | m.assert_called_with(expected, [ 372 | ('Content-Type', 'text/plain; charset=UTF-8'), 373 | ('Content-Length', str(len(expected))) 374 | ]) 375 | self.assertEqual(r, [expected.encode()]) 376 | 377 | def test_download_with_filename_argument(self): 378 | app = ice.Ice() 379 | 380 | expected1 = 'foo' 381 | expected2 = 'echo "hi"' 382 | 383 | @app.get('/') 384 | def foo(): 385 | return app.download(expected1, 'foo.txt') 386 | 387 | @app.get('/bar') 388 | def bar(): 389 | return app.download(expected2, 'foo.sh', 'text/plain') 390 | 391 | m = unittest.mock.Mock() 392 | 393 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 394 | m.assert_called_with('200 OK', [ 395 | ('Content-Disposition', 'attachment; filename="foo.txt"'), 396 | ('Content-Type', 'text/plain; charset=UTF-8'), 397 | ('Content-Length', str(len(expected1))) 398 | ]) 399 | self.assertEqual(r, [expected1.encode()]) 400 | 401 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/bar'}, m) 402 | m.assert_called_with('200 OK', [ 403 | ('Content-Disposition', 'attachment; filename="foo.sh"'), 404 | ('Content-Type', 'text/plain; charset=UTF-8'), 405 | ('Content-Length', str(len(expected2))) 406 | ]) 407 | self.assertEqual(r, [expected2.encode()]) 408 | 409 | def test_download_without_filename_argument(self): 410 | app = ice.Ice() 411 | 412 | @app.get('/') 413 | def foo(): 414 | return app.download('foo') 415 | 416 | m = unittest.mock.Mock() 417 | expected = 'foo' 418 | 419 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/foo.txt'}, m) 420 | m.assert_called_with('200 OK', [ 421 | ('Content-Disposition', 'attachment; filename="foo.txt"'), 422 | ('Content-Type', 'text/plain; charset=UTF-8'), 423 | ('Content-Length', str(len(expected))) 424 | ]) 425 | self.assertEqual(r, [expected.encode()]) 426 | 427 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/foo.css'}, m) 428 | m.assert_called_with('200 OK', [ 429 | ('Content-Disposition', 'attachment; filename="foo.css"'), 430 | ('Content-Type', 'text/css; charset=UTF-8'), 431 | ('Content-Length', str(len(expected))) 432 | ]) 433 | self.assertEqual(r, [expected.encode()]) 434 | 435 | def test_download_without_filename(self): 436 | app = ice.Ice() 437 | 438 | @app.get('/') 439 | def foo(): 440 | return app.download('foo') 441 | 442 | m = unittest.mock.Mock() 443 | expected = 'foo' 444 | with self.assertRaises(ice.LogicError) as cm: 445 | app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 446 | self.assertEqual(str(cm.exception), 447 | 'Cannot determine filename for download') 448 | 449 | def test_download_static(self): 450 | app = ice.Ice() 451 | 452 | @app.get('/') 453 | def foo(): 454 | return app.download(app.static(data.dirpath, 'foo.txt')) 455 | 456 | m = unittest.mock.Mock() 457 | expected = 'foo\n' 458 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 459 | m.assert_called_with('200 OK', [ 460 | ('Content-Disposition', 'attachment; filename="foo.txt"'), 461 | ('Content-Type', 'text/plain; charset=UTF-8'), 462 | ('Content-Length', str(len(expected))) 463 | ]) 464 | self.assertEqual(r, [expected.encode()]) 465 | 466 | def test_download_with_status_code(self): 467 | app = ice.Ice() 468 | 469 | # Return an error from app.static. 470 | @app.get('/') 471 | def foo(): 472 | return app.download(app.static(data.dirpath, 'nonexistent.txt')) 473 | 474 | # Return an error status code. 475 | @app.get('/bar') 476 | def bar(): 477 | return app.download(410) 478 | 479 | # Set body and return status code 200. 480 | @app.get('/baz') 481 | def baz(): 482 | app.response.body = 'baz' 483 | return app.download(200, 'baz.txt') 484 | 485 | m = unittest.mock.Mock() 486 | 487 | expected = '404 Not Found' 488 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/'}, m) 489 | m.assert_called_with(expected, [ 490 | ('Content-Type', 'text/plain; charset=UTF-8'), 491 | ('Content-Length', str(len(expected))) 492 | ]) 493 | self.assertEqual(r, [expected.encode()]) 494 | 495 | expected = '410 Gone' 496 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/bar'}, m) 497 | m.assert_called_with(expected, [ 498 | ('Content-Type', 'text/plain; charset=UTF-8'), 499 | ('Content-Length', str(len(expected))) 500 | ]) 501 | self.assertEqual(r, [expected.encode()]) 502 | 503 | expected = 'baz' 504 | r = app({'REQUEST_METHOD': 'GET', 'PATH_INFO': '/baz'}, m) 505 | m.assert_called_with('200 OK', [ 506 | ('Content-Disposition', 'attachment; filename="baz.txt"'), 507 | ('Content-Type', 'text/plain; charset=UTF-8'), 508 | ('Content-Length', str(len(expected))) 509 | ]) 510 | self.assertEqual(r, [expected.encode()]) 511 | -------------------------------------------------------------------------------- /test/test_wildcard_route.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2014-2017 Susam Pal 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """Tests for class ice.WildcardRoute.""" 26 | 27 | 28 | import unittest 29 | from unittest import mock 30 | import ice 31 | 32 | class WildcardRouteTest(unittest.TestCase): 33 | def test_tokens(self): 34 | self.assertEqual(ice.WildcardRoute.tokens(''), []) 35 | self.assertEqual(ice.WildcardRoute.tokens('/'), ['/']) 36 | self.assertEqual(ice.WildcardRoute.tokens('//'), ['/', '/']) 37 | self.assertEqual(ice.WildcardRoute.tokens('/foo//bar'), 38 | ['/', 'foo', '/', '/', 'bar']) 39 | self.assertEqual(ice.WildcardRoute.tokens('foo/'), ['foo', '/']) 40 | self.assertEqual(ice.WildcardRoute.tokens('/foo'), ['/', 'foo']) 41 | self.assertEqual(ice.WildcardRoute.tokens('/foo/bar'), 42 | ['/', 'foo', '/', 'bar']) 43 | self.assertEqual(ice.WildcardRoute.tokens('/foo/bar/'), 44 | ['/', 'foo', '/', 'bar', '/']) 45 | self.assertEqual(ice.WildcardRoute.tokens('/foo/<>'), 46 | ['/', 'foo', '/', '<>']) 47 | self.assertEqual(ice.WildcardRoute.tokens('/foo/<>/'), 48 | ['/', 'foo', '/', '<>', '/']) 49 | self.assertEqual(ice.WildcardRoute.tokens('/foo/<>-<>'), 50 | ['/', 'foo', '/', '<>', '-', '<>']) 51 | self.assertEqual(ice.WildcardRoute.tokens('/foo/Home
') 76 | 77 | @app.get('/foo') 78 | def foo(): 79 | return ('' 80 | 'Foo
') 82 | 83 | # Test 84 | self.run_app() 85 | self.assert200('/', 'Home
') 86 | self.assert200('/foo', 'Foo
') 87 | self.assert404('/foo/') 88 | self.assert404('/bar') 89 | 90 | def test_anonymous_wildcard_example(self): 91 | app = self.app 92 | 93 | # Example 94 | @app.get('/<>') 95 | def foo(a): 96 | return ('' 97 | '' + a + '
') 99 | 100 | # Test 101 | self.run_app() 102 | self.assert200('/foo', 'foo
') 103 | self.assert200('/bar', 'bar
') 104 | self.assert404('/foo/') 105 | self.assert404('/foo/bar') 106 | 107 | def test_named_wildcard_example1(self): 108 | app = self.app 109 | 110 | # Example 111 | @app.get('/') 112 | def foo(a): 113 | return ('' 114 | '' + a + '
') 116 | 117 | # Test 118 | self.run_app() 119 | self.assert200('/foo', 'foo
') 120 | self.assert200('/bar', 'bar
') 121 | self.assert404('/foo/') 122 | self.assert404('/foo/bar') 123 | 124 | def test_named_wildcard_example2(self): 125 | app = self.app 126 | 127 | # Example 128 | @app.get('/foo/<>-<>/-/<>-args: {}
kwargs: {}
page_id: {}
user: {}
category: {}
page_id: python
user: snowman
'
156 | 'category: articles
args: {}
kwargs: {}
args: ()
kwargs: {}
page_id: ' + page_id + '
' 182 | '') 183 | 184 | # Test 185 | self.run_app() 186 | self.assert200('/snowman/articles/python', 187 | 'page_id: python
') 188 | 189 | def test_wildcard_specification_example(self): 190 | app = self.app 191 | 192 | # Example 193 | @app.get('/notes/<:path>/<:int>') 194 | def note(note_path, note_id): 195 | return ('' 196 | 'note_path: {}
note_id: {}
note_path: tech/python
note_id: 12
note_path: tech/python
note_id: 0
' + a + '
') 219 | 220 | # Test 221 | self.run_app() 222 | self.assert200('/foo', 'foo
') 223 | self.assert200('/foo/bar/', 'foo/bar/
') 224 | 225 | def test_regex_route_example2(self): 226 | app = self.app 227 | 228 | # Example 229 | @app.get('/(?Ppage_id: {}
user: {}
category: {}
page_id: python
user: snowman
'
240 | 'category: articles
Foo
') 251 | 252 | # Test 253 | self.run_app() 254 | self.assert200('/Foo
') 255 | self.assert404('/foo') 256 | 257 | def test_explicit_wildcard_route_example(self): 258 | # Example 259 | app = self.app 260 | @app.get('wildcard:/(foo)/<>') 261 | def foo(a): 262 | return ('' 263 | 'a: ' + a + '
') 265 | 266 | # Test 267 | self.run_app() 268 | self.assert200('/(foo)/bar', 'a: bar
') 269 | self.assert404('/foo/<>') 270 | 271 | def test_explicit_regex_route_example(self): 272 | app = self.app 273 | 274 | # Example 275 | @app.get('regex:/foo\d*$') 276 | def foo(): 277 | return ('' 278 | 'Foo
') 280 | 281 | # Test 282 | self.run_app() 283 | self.assert200('/foo123', 'Foo
') 284 | self.assert200('/foo', 'Foo
') 285 | self.assert404('/foo\d*$') 286 | 287 | def test_query_string_example1(self): 288 | app = self.app 289 | 290 | # Example 291 | @app.get('/') 292 | def home(): 293 | return ('' 294 | 'name: {}
' 296 | '').format(app.request.query['name']) 297 | 298 | # Test 299 | self.run_app() 300 | self.assert200('/?name=Humpty+Dumpty', 301 | 'name: Humpty Dumpty
') 302 | 303 | def test_query_string_example2(self): 304 | app = self.app 305 | 306 | # Example 307 | @app.get('/') 308 | def home(): 309 | return ('' 310 | 'name: {}
' 312 | '').format(app.request.query.getall('name')) 313 | 314 | # Test 315 | self.run_app() 316 | self.assert200('/?name=Humpty&name=Santa', 317 | "name: ['Humpty', 'Santa']
") 318 | 319 | def test_form_example1(self): 320 | app = self.app 321 | 322 | # Example 323 | @app.get('/') 324 | def show_form(): 325 | return ('' 326 | 'First name: {}
Last name: {}
First name: Humpty
Last name: Dumpty
name (single): {}
name (multi): {}
name (single): Santa
'
381 | b"name (multi): ['Humpty', 'Santa']
Count: {}
'.format(count)) 394 | 395 | # Test 396 | self.run_app() 397 | response = urllib.request.urlopen('http://localhost:8080/') 398 | self.assertEqual(response.getheader('Set-Cookie'), 'count=1') 399 | self.assertIn(b'Count: 1
', response.read()) 400 | 401 | response = urllib.request.urlopen( 402 | urllib.request.Request('http://localhost:8080/', 403 | headers={'Cookie': 'count=1'})) 404 | self.assertEqual(response.getheader('Set-Cookie'), 'count=2') 405 | self.assertIn(b'Count: 2
', response.read()) 406 | 407 | 408 | def test_error_example(self): 409 | app = self.app 410 | 411 | # Example 412 | @app.error(404) 413 | def error(): 414 | return ('' 415 | 'Page not found
') 417 | 418 | # Test 419 | self.run_app() 420 | with self.assertRaises(urllib.error.HTTPError) as cm: 421 | urllib.request.urlopen('http://localhost:8080/foo') 422 | self.assertEqual(cm.exception.code, 404) 423 | self.assertIn(b'Page not found
', cm.exception.read()) 424 | 425 | # Set status code and return body 426 | def test_status_codes_example1(self): 427 | app = self.app 428 | 429 | # Example 430 | @app.get('/foo') 431 | def foo(): 432 | app.response.status = 403 433 | return ('' 434 | 'Access is forbidden
') 436 | 437 | # Test 438 | self.run_app() 439 | with self.assertRaises(urllib.error.HTTPError) as cm: 440 | urllib.request.urlopen('http://localhost:8080/foo') 441 | self.assertEqual(cm.exception.code, 403) 442 | self.assertIn(b'Access is forbidden
', cm.exception.read()) 443 | 444 | # Set body and return status code (not recommended) 445 | def test_status_code_example2(self): 446 | app = self.app 447 | 448 | # Example 449 | @app.get('/foo') 450 | def foo(): 451 | app.response.body = ('' 452 | 'Access is forbidden
') 454 | return 403 455 | 456 | # Test 457 | self.run_app() 458 | with self.assertRaises(urllib.error.HTTPError) as cm: 459 | urllib.request.urlopen('http://localhost:8080/foo') 460 | self.assertEqual(cm.exception.code, 403) 461 | self.assertIn(b'Access is forbidden
', cm.exception.read()) 462 | 463 | # Set status code and error handler (recommended) 464 | def test_status_code_example3(self): 465 | app = self.app 466 | 467 | # Example 468 | @app.get('/foo') 469 | def foo(): 470 | return 403 471 | 472 | @app.error(403) 473 | def error403(): 474 | return ('' 475 | 'Access is forbidden
') 477 | 478 | # Test 479 | self.run_app() 480 | with self.assertRaises(urllib.error.HTTPError) as cm: 481 | urllib.request.urlopen('http://localhost:8080/foo') 482 | self.assertEqual(cm.exception.code, 403) 483 | self.assertIn(b'Access is forbidden
', cm.exception.read()) 484 | 485 | # Set return code only (generic error handler is invoked) 486 | def test_status_code_example4(self): 487 | app = self.app 488 | 489 | # Example 490 | @app.get('/foo') 491 | def foo(): 492 | return 403 493 | 494 | # Test 495 | self.run_app() 496 | with self.assertRaises(urllib.error.HTTPError) as cm: 497 | urllib.request.urlopen('http://localhost:8080/foo') 498 | self.assertEqual(cm.exception.code, 403) 499 | self.assertIn(b'Request forbidden ' 500 | b'-- authorization will not help
\n', 501 | cm.exception.read()) 502 | 503 | def test_redirect_example1(self): 504 | app = self.app 505 | 506 | @app.get('/foo') 507 | def foo(): 508 | return 303, '/bar' 509 | 510 | @app.get('/bar') 511 | def bar(): 512 | return ('' 513 | 'Bar
') 515 | 516 | self.run_app() 517 | response = urllib.request.urlopen('http://localhost:8080/foo') 518 | self.assertIn(b'Bar
', response.read()) 519 | 520 | def test_redirect_example2(self): 521 | app = self.app 522 | 523 | @app.get('/foo') 524 | def foo(): 525 | app.response.add_header('Location', '/bar') 526 | return 303 527 | 528 | @app.get('/bar') 529 | def bar(): 530 | return ('' 531 | 'Bar
') 533 | 534 | self.run_app() 535 | response = urllib.request.urlopen('http://localhost:8080/foo') 536 | self.assertIn(b'Bar
', response.read()) 537 | 538 | # Static file with media type guessing 539 | def test_static_file_example1(self): 540 | app = self.app 541 | 542 | # Example 543 | @app.get('/code/<:path>') 544 | def send_code(path): 545 | return app.static(data.dirpath, path) 546 | 547 | # Test regular request 548 | self.run_app() 549 | response = urllib.request.urlopen('http://localhost:8080/code/foo.txt') 550 | self.assertEqual(response.read(), b'foo\n') 551 | self.assertEqual(response.getheader('Content-Type'), 552 | 'text/plain; charset=UTF-8') 553 | 554 | # Test directory traversal attack 555 | with self.assertRaises(urllib.error.HTTPError) as cm: 556 | urllib.request.urlopen('http://localhost:8080/code/%2e%2e/foo.txt') 557 | self.assertEqual(cm.exception.code, 403) 558 | 559 | # Static file with explicit media type 560 | def test_static_file_example2(self): 561 | app = self.app 562 | 563 | # Example 564 | @app.get('/code/<:path>') 565 | def send_code(path): 566 | return app.static(data.dirpath, path, 567 | media_type='text/plain', charset='ISO-8859-1') 568 | 569 | # Test regular request 570 | self.run_app() 571 | response = urllib.request.urlopen('http://localhost:8080/code/foo.c') 572 | self.assertEqual(b'#include{}
'.format(user_agent)) 662 | 663 | # Test 664 | self.run_app() 665 | self.assert200('/', 'Python-urllib') 666 | -------------------------------------------------------------------------------- /ice.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2014-2017 Susam Pal 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """Ice - WSGI on the rocks. 26 | 27 | Ice is a simple and tiny WSGI microframework meant for developing small 28 | Python web applications. 29 | """ 30 | 31 | 32 | __version__ = '0.0.2' 33 | __date__ = '25 March 2014' 34 | __author__ = 'Susam PalThis is the default ice web page.
') 70 | 71 | @app.error() 72 | def generic_error_page(): 73 | """Return a simple and generic error page.""" 74 | return simple_html(app.response.status_line, 75 | '{description}
\n' 77 | 'Home
') 101 | 102 | @app.get('/foo') 103 | def foo(): 104 | return ('' 105 | 'Foo
') 107 | 108 | if __name__ == '__main__': 109 | app.run() 110 | 111 | The routes defined in the above example are called literal routes 112 | because they match the request path exactly as specified in the argument 113 | to ``app.get`` decorator. Routes defined with the ``app.get`` decorator 114 | matches HTTP GET requests. Now, visiting http://localhost:8080/ displays 115 | a page with the following text. 116 | 117 | | Home 118 | 119 | Visiting http://localhost:8080/foo displays a page with the following 120 | text. 121 | 122 | | Foo 123 | 124 | However, visiting http://localhost:8080/foo/ or 125 | http://localhost:8080/foo/bar displays the '404 Not Found' page because 126 | the literal pattern ``'/foo'`` does not match the request path 127 | ``'/foo/'`` or ``'/foo/bar'``. 128 | 129 | Wildcard Routes 130 | ~~~~~~~~~~~~~~~ 131 | Anonymous Wildcards 132 | ''''''''''''''''''' 133 | The following code example is the simplest application demonstrating a 134 | wildcard route that matches request path of the form ``/`` followed by 135 | any string devoid of ``/``, ``<`` and ``>``. The characters ``<>`` is an 136 | anonymous wildcard because there is no name associated with this 137 | wildcard. The part of the request path matched by an anonymous wildcard 138 | is passed as a positional argument to the route's handler. 139 | 140 | .. code:: python 141 | 142 | import ice 143 | app = ice.cube() 144 | 145 | @app.get('/<>') 146 | def foo(a): 147 | return ('' 148 | '' + a + '
') 150 | 151 | if __name__ == '__main__': 152 | app.run() 153 | 154 | Save the above code in a file and execute it with Python interpreter. 155 | Then open your browser, visit http://localhost:8080/foo, and you should 156 | be able to see a page with the followning text. 157 | 158 | | foo 159 | 160 | If you visit http://localhost:8080/bar instead, you should see a page 161 | with the following text. 162 | 163 | | bar 164 | 165 | However, visiting http://localhost:8080/foo/ or 166 | http://localhost:8080/foo/bar displays the '404 Not Found' page because 167 | the wildcard based pattern ``/<>`` does not match ``/foo/`` or 168 | ``/foo/bar``. 169 | 170 | Named Wildcards 171 | ''''''''''''''' 172 | A wildcard with a valid Python identifier as its name is called a named 173 | wildcard. The part of the request path matched by a named wildcard is 174 | passed as a keyword argument, with the same name as that of the 175 | wildcard, to the route's handler. 176 | 177 | .. code:: python 178 | 179 | import ice 180 | app = ice.cube() 181 | 182 | @app.get('/') 183 | def foo(a): 184 | return ('' 185 | '' + a + '
') 187 | 188 | if __name__ == '__main__': 189 | app.run() 190 | 191 | The ``a``, in ````, is the name of the wildcard. The ice application 192 | in this example with a named wildcard behaves similar to the earlier one 193 | with an anonymous wildcard. The following example code clearly 194 | demonstrates how matches due to anonymous wildcards are passed 195 | differently from the matches due to named wildcards. 196 | 197 | .. code:: python 198 | 199 | import ice 200 | app = ice.cube() 201 | 202 | @app.get('/foo/<>-<>/-/<>-args: {}
kwargs: {}
page_id: {}
user: {}
category: {}
args: {}
kwargs: {}
page_id: ' + page_id + '
' 293 | '') 294 | 295 | if __name__ == '__main__': 296 | app.run() 297 | 298 | After running this application, visiting 299 | http://localhost:8080/snowman/articles/python displays a page with 300 | the following text. 301 | 302 | | page_id: python 303 | 304 | There are three wildcards in the route's request path pattern but there 305 | is only one argument in the route's handler because two out of the 306 | three wildcards are throwaway wildcards. 307 | 308 | Wildcard Specification 309 | '''''''''''''''''''''' 310 | The complete syntax of a wildcard specification is: <*name*:*type*>. 311 | 312 | The following rules describe how a wildcard is interpreted. 313 | 314 | 1. The delimiters ``<`` (less-than sign) and ``>`` (greater-than sign), 315 | are mandatory. 316 | 2. However, *name*, ``:`` (colon) and *type* are optional. 317 | 3. Either a valid Python identifier or the exclamation mark, ``!``, 318 | must be specified as *name*. 319 | 4. If *name* is missing, the part of the request path matched by the 320 | wildcard is passed as a positional argument to the route's handler. 321 | 5. If *name* is present and it is a valid Python identifier, the part 322 | of the request path matched by the wildcard is passed as a keyword 323 | argument to the route's handler. 324 | 6. If *name* is present and it is ``!``, the part of the request path 325 | matched by the wildcard is not passed to the route's handler. 326 | 7. If *name* is present but it is neither ``!`` nor a valid Python 327 | identifier, ice.RouteError is raised. 328 | 8. If *type* is present, it must be preceded by ``:`` (colon). 329 | 9. If *type* is present but it is not ``str``, ``path``, ``int``, 330 | ``+int`` and ``-int``, ice.RouteError is raised. 331 | 10. If *type* is missing, it is assumed to be ``str``. 332 | 11. If *type* is ``str``, it matches a string of one or more characters 333 | such that none of the characters is ``/``. The path of the request 334 | path matched by the wildcard is passed as an ``str`` object to the 335 | route's handler. 336 | 12. If *type* is ``path``, it matches a string of one or more characters 337 | that may contain ``/``. The path of the request path matched by the 338 | wildcard is passed as an ``str`` object to the route's handler. 339 | 13. If *type* is ``int``, ``+int`` or ``-int``, the path of the request 340 | path matched by the wildcard is passed as an ``int`` object to the 341 | route's handler. 342 | 14. If *type* is ``+int``, the wildcard matches a positive integer 343 | beginning with a non-zero digit. 344 | 15. If *type* is ``int``, the wildcard matches ``0`` as well as 345 | everything that a wildcard of type ``+int`` matches. 346 | 16. If *type* is ``-int``, the wildcard matches a negative integer that 347 | begins with the ``-`` sign followed by a non-zero digit as well as 348 | everything that a wildcard of type ``int`` matches. 349 | 350 | Here is an example that demonstrates a typical route with ``path`` and 351 | ``int`` wildcards. 352 | 353 | .. code:: python 354 | 355 | import ice 356 | app = ice.cube() 357 | 358 | @app.get('/notes/<:path>/<:int>') 359 | def note(note_path, note_id): 360 | return ('' 361 | 'note_path: {}
note_id: {}
' + a + '
') 404 | 405 | if __name__ == '__main__': 406 | app.run() 407 | 408 | After running this application, visiting http://localhost:8080/foo 409 | displays a page with the following text. 410 | 411 | | foo 412 | 413 | Visiting http://localhost:8080/foo/bar/ displays a page with the 414 | following text. 415 | 416 | | foo/bar/ 417 | 418 | The part of the request path matched by a symbolic capturing group in 419 | the regular expression is passed as a keyword argument with the same 420 | name as that of the symbolic group. 421 | 422 | .. code:: python 423 | 424 | import ice 425 | app = ice.cube() 426 | 427 | @app.get('/(?Ppage_id: {}
user: {}
category: {}
Foo
') 498 | 499 | if __name__ == '__main__': 500 | app.run() 501 | 502 | After running this application, visiting 503 | http://localhost:8080/%3Cfoo%3E displays a page containing the 504 | following text. 505 | 506 | | Foo 507 | 508 | A request path pattern that seems to contain a wildcard or a capturing 509 | group but needs to be treated as a literal pattern must be prefixed with 510 | the string ``literal:``. 511 | 512 | Explicit Wildcard Routes 513 | '''''''''''''''''''''''' 514 | To define a wildcard route with the request path pattern as 515 | ``/(foo)/<>``, the ``wildcard:`` prefix must be used. Without it, the 516 | pattern is interpreted as a regular expression pattern because the 517 | ``(foo)`` in the pattern looks like a regular expression capturing 518 | group. 519 | 520 | .. code:: python 521 | 522 | import ice 523 | app = ice.cube() 524 | 525 | @app.get('wildcard:/(foo)/<>') 526 | def foo(a): 527 | return ('' 528 | 'a: ' + a + '
') 530 | 531 | if __name__ == '__main__': 532 | app.run() 533 | 534 | After running this application, visiting http://localhost:8080/(foo)/bar 535 | displays a page with the following text. 536 | 537 | | a: bar 538 | 539 | A request path pattern that seems to contain a regular expression 540 | capturing group but needs to be treated as a wildcard pattern must be 541 | prefixed with the string ``wildcard:``. 542 | 543 | Explicit Regular Expression Routes 544 | '''''''''''''''''''''''''''''''''' 545 | To define a regular expression route with the request path pattern as 546 | ``^/foo\d*$``, the ``regex:`` prefix must be used. Without it, the 547 | pattern is interpreted as a literal pattern because there is no 548 | capturing group in the pattern. 549 | 550 | .. code:: python 551 | 552 | import ice 553 | app = ice.cube() 554 | 555 | @app.get('regex:/foo\d*$') 556 | def foo(): 557 | return ('' 558 | 'Foo
') 560 | 561 | if __name__ == '__main__': 562 | app.run() 563 | 564 | After running this application, visiting http://localhost:8080/foo or 565 | http://localhost:8080/foo123 displays a page containing the following 566 | text. 567 | 568 | | Foo 569 | 570 | A request path pattern that does not contain a regular expression 571 | capturing group but needs to be treated as a regular expression pattern 572 | must be prefixed with the string ``regex:``. 573 | 574 | 575 | Query Strings 576 | ------------- 577 | The following example shows an application that can process a query 578 | string in a GET request. 579 | 580 | .. code:: python 581 | 582 | import ice 583 | app = ice.cube() 584 | 585 | @app.get('/') 586 | def home(): 587 | return ('' 588 | 'name: {}
' 590 | '').format(app.request.query['name']) 591 | 592 | if __name__ == '__main__': 593 | app.run() 594 | 595 | After running this application, visiting 596 | http://localhost:8080/?name=Humpty+Dumpty displays a page with the 597 | following text. 598 | 599 | | name: Humpty Dumpty 600 | 601 | Note that the ``+`` sign in the query string has been properly URL 602 | decoded into a space. 603 | 604 | The ``app.request.query`` object in the code is an ``ice.MultiDict`` 605 | object that can store multiple values for every key. However, when used 606 | like a dictionary, it returns the most recently added value for a key. 607 | Therefore, visiting http://localhost:8080/?name=Humpty&name=Santa 608 | displays a page with the following text. 609 | 610 | | name: Santa 611 | 612 | Note that in this URL, there are two values passed for the ``name`` 613 | field in the query string, but accessing ``app.request.query['name']`` 614 | provides us only the value that is most recently added. To get all the 615 | values for a key in ``app.request.query``, we can use the 616 | ``ice.MultiDict.getall`` method as shown below. 617 | 618 | .. code:: python 619 | 620 | import ice 621 | app = ice.cube() 622 | 623 | @app.get('/') 624 | def home(): 625 | return ('' 626 | 'name: {}
' 628 | '').format(app.request.query.getall('name')) 629 | 630 | if __name__ == '__main__': 631 | app.run() 632 | 633 | Now, visiting http://localhost:8080/?name=Humpty&name=Santa 634 | displays a page with the following text. 635 | 636 | | name: ['Humpty', 'Santa'] 637 | 638 | Note that the ``ice.MultiDict.getall`` method returns all the values 639 | belonging to the key as a ``list`` object. 640 | 641 | 642 | Forms 643 | ----- 644 | The following example shows an application that can process forms 645 | submitted by a POST request. 646 | 647 | .. code:: python 648 | 649 | import ice 650 | app = ice.cube() 651 | 652 | @app.get('/') 653 | def show_form(): 654 | return ('' 655 | 'First name: {}
Last name: {}
name (single): {}
name (multi): {}
Count: {}
'.format(count)) 730 | 731 | app.run() 732 | 733 | The ``app.request.cookies`` object in this code, like the 734 | ``app.request.query`` object in a previous section, is a MultiDict 735 | object. Every cookie name and value sent by the client to the 736 | application found in the HTTP Cookie header is available in this object 737 | as key value pairs. 738 | 739 | The ``app.response.set_cookie`` method is used to set cookies to be sent 740 | from the application to the client. 741 | 742 | 743 | Error Pages 744 | ----------- 745 | The application object returned by the ``ice.cube`` function contains a 746 | generic fallback error handler that returns a simple error page with the 747 | HTTP status line, a short description of the status and the version of 748 | the ice module. 749 | 750 | This error handler may be overridden using the ``error`` decorator. This 751 | decorator accepts one optional integer argument that may be used to 752 | explicitly specify the HTTP status code of responses for which the 753 | handler should be invoked to generate an error page. If no argument is 754 | provided, the error handler is defined as a fallback error handler. A 755 | fallback error handler is invoked to generate an error page for any HTTP 756 | response representing an error when there is no error handler defined 757 | explicitly for the response status code of the HTTP response. 758 | 759 | Here is an example. 760 | 761 | .. code:: python 762 | 763 | import ice 764 | app = ice.cube() 765 | 766 | @app.error(404) 767 | def error(): 768 | return ('' 769 | 'Page not found
') 771 | 772 | if __name__ == '__main__': 773 | app.run() 774 | 775 | After running this application, visiting http://localhost:8080/foo 776 | displays a page with the following text. 777 | 778 | | Page not found 779 | 780 | 781 | .. _status-codes: 782 | 783 | Status Codes 784 | ------------ 785 | In all the examples above, the response message body is returned as a 786 | string from a route's handler. It is also possible to return the 787 | response status code as an integer. In other words, a route's handler 788 | must either return a string or an integer. When a string is returned, it 789 | is sent as response message body to the client. When an integer is 790 | returned and it is a valid HTTP status code, an HTTP response with this 791 | status code is sent to the client. If the value returned by a route's 792 | handler is neither a string nor an integer representing a valid HTTP 793 | status code, then an error is raised. 794 | 795 | Therefore there are two ways to return an HTTP response from a route's 796 | handler. 797 | 798 | 1. Return message body and optionally set status code. This is the 799 | preferred way of returning content for normal HTTP responses (200 800 | OK). If the status code is not set explicitly in a route's handler, 801 | then it has a default value of 200. 802 | 2. Return status code and optionally set message body. This is the 803 | preferred way of returning content for HTTP errors. If the message 804 | body is not set explicitly in a route's handler, then the error 805 | handler for the returned status code is invoked to return a message 806 | body. 807 | 808 | Here is an example where status code is set to 403 and a custom 809 | error page is returned. 810 | 811 | .. code:: python 812 | 813 | import ice 814 | 815 | app = ice.cube() 816 | 817 | @app.get('/foo') 818 | def foo(): 819 | app.response.status = 403 820 | return ('' 821 | 'Access is forbidden
') 823 | 824 | if __name__ == '__main__': 825 | app.run() 826 | 827 | After running this application, visiting http://localhost:8080/foo 828 | displays a page with the following text. 829 | 830 | | Access is forbidden 831 | 832 | Here is another way of writing the above application. In this case, the 833 | message body is set and the status code is returned. 834 | 835 | .. code:: python 836 | 837 | import ice 838 | 839 | app = ice.cube() 840 | 841 | @app.get('/foo') 842 | def foo(): 843 | app.response.body = ('' 844 | 'Access is forbidden
') 846 | return 403 847 | 848 | if __name__ == '__main__': 849 | app.run() 850 | 851 | Although the above way of setting message body works, using an error 852 | handler is the preferred way of defining the message body for an HTTP 853 | error. Here is an example that demonstrates this. 854 | 855 | .. code:: python 856 | 857 | import ice 858 | 859 | app = ice.cube() 860 | 861 | @app.get('/foo') 862 | def foo(): 863 | return 403 864 | 865 | @app.error(403) 866 | def error403(): 867 | return ('' 868 | 'Access is forbidden
') 870 | 871 | if __name__ == '__main__': 872 | app.run() 873 | 874 | For simple web applications, just returning the status code is 875 | sufficient. When neither a message body is defined nor an error handler 876 | is defined, a generic fallback error handler set in the application 877 | object returned by the ``ice.cube`` is used to return a simple error 878 | page with the HTTP status line, a short description of the status and 879 | the version of the ice module. 880 | 881 | .. code:: python 882 | 883 | import ice 884 | 885 | app = ice.cube() 886 | 887 | @app.get('/foo') 888 | def foo(): 889 | return 403 890 | 891 | if __name__ == '__main__': 892 | app.run() 893 | 894 | After running this application, visiting http://localhost:8080/foo 895 | displays a page with the following text. 896 | 897 | | 403 Forbidden 898 | | Request forbidden -- authorization will not help 899 | 900 | 901 | Redirects 902 | --------- 903 | Here is an example that demonstrates how to redirect a client to a 904 | different URL. 905 | 906 | .. code:: python 907 | 908 | import ice 909 | app = ice.cube() 910 | 911 | @app.get('/foo') 912 | def foo(): 913 | return 303, '/bar' 914 | 915 | @app.get('/bar') 916 | def bar(): 917 | return ('' 918 | 'Bar
') 920 | 921 | app.run() 922 | 923 | After running this application, visiting http://localhost:8080/foo 924 | with a browser redirects the browser to http://localhost:8080/bar and 925 | displays a page with the following text. 926 | 927 | | Bar 928 | 929 | To send a redirect, the route handler needs to return a tuple such that 930 | the first item in the tuple is an HTTP status code for redirection and 931 | the second item is the URL to which the client should be redirected to. 932 | 933 | The behaviour of the above code is equivalent to the following code. 934 | 935 | .. code:: python 936 | 937 | import ice 938 | app = ice.cube() 939 | 940 | @app.get('/foo') 941 | def foo(): 942 | app.response.add_header('Location', '/bar') 943 | return 303 944 | 945 | @app.get('/bar') 946 | def bar(): 947 | return ('' 948 | 'Bar
') 950 | 951 | app.run() 952 | 953 | Much of the discussion in the :ref:`status-codes` section applies to 954 | this section too, i.e. it is possible to set the status code in 955 | ``app.response.status``, add a Location header and return a message 956 | body, or add a Location header, set the message body in 957 | ``app.response.body`` and return a status code. However, returning a 958 | tuple of redirection status code and URL, as shown in the first example 959 | in this section, is the simplest and preferred way to send a redirect. 960 | 961 | 962 | .. _static-files: 963 | 964 | Static Files 965 | ------------ 966 | In a typical production environment, a web server may be configured to 967 | receive HTTP requests and forward it to a Python application via WSGI. 968 | In such a setup, it might make more sense to configure the web server to 969 | serve static files because web servers implement several standard file 970 | handling capabilities and response headers, e.g. 'Last-Modified', 971 | 'If-Modified-Since', etc. However, it is possible to serve static files 972 | from an ice application using :meth:`ice.Ice.static` that provides a 973 | very rudimentary means of serving static files. This could be useful in 974 | a development environment where one would want to test pages with static 975 | content such as style sheets, images, etc. served by an ice application 976 | without using a web server. 977 | 978 | .. automethod:: ice.Ice.static 979 | :noindex: 980 | 981 | Here is an example. 982 | 983 | .. code:: python 984 | 985 | import ice 986 | app = ice.cube() 987 | 988 | @app.get('/code/<:path>') 989 | def send_code(path): 990 | return app.static('/var/www/project/code', path) 991 | 992 | if __name__ == '__main__': 993 | app.run() 994 | 995 | If there is a file called /var/www/project/code/data/foo.txt, then 996 | visiting http://localhost:8080/code/data/foo.txt would return the 997 | content of this file as response. 998 | 999 | However, visiting http://localhost:8080/code/%2e%2e/foo.txt would 1000 | display a '403 Forbidden' page because this request attempts to access 1001 | foo.txt in the parent directory of the document root directory 1002 | (``%2e%2d`` is the URL encoding of ``..``). This is not allowed in order 1003 | to prevent `directory traversal attack`_. 1004 | 1005 | .. _directory traversal attack: https://en.wikipedia.org/wiki/Directory_traversal_attack 1006 | 1007 | In the above example, the 'Content-Type' header of the response is 1008 | automatically set to 'text/plain; charset=UTF-8'. With only two 1009 | arguments specified to this method, it uses the extension name of the 1010 | file being returned to automatically guess the media type to be used in 1011 | the 'Content-Type' header. For example, the media type of a .txt file is 1012 | typically *guessed* to be 'text/plain'. But this may be different 1013 | because system configuration files may be referred in order to guess the 1014 | media type and such configuration files may map a .txt file to a 1015 | different media type. 1016 | 1017 | For example, on a Debian 8.0 system, /etc/mime.types maps a .c file to 1018 | 'text/x-csrc'. This is one of the files that is referred to guess the 1019 | media type. Therefore, the 'Content-Type' header for a request to 1020 | http://localhost:8080/code/data/foo.c would be set to 1021 | 'text/x-csrc; charset=UTF-8' on such a system. 1022 | 1023 | To see the list of files that may be referred to guess media type, 1024 | execute this command. :: 1025 | 1026 | python3 -c "import mimetypes; print(mimetypes.knownfiles)" 1027 | 1028 | The media type of static file being returned in a response can be set 1029 | explicitly to a desired value using the ``media_type`` keyword argument. 1030 | 1031 | The charset defaults to 'UTF-8' for any media type of type 'text' 1032 | regardless of the subtype. This may be changed with the ``charset`` 1033 | keyword argument. 1034 | 1035 | .. code:: python 1036 | 1037 | import ice 1038 | app = ice.cube() 1039 | 1040 | @app.get('/code/<:path>') 1041 | def send_code(path): 1042 | return app.static('/var/www/project/code', path, 1043 | media_type='text/plain', charset='ISO-8859-1') 1044 | 1045 | if __name__ == '__main__': 1046 | app.run() 1047 | 1048 | The above code guarantees that the 'Content-Type' header of a request to 1049 | http://localhost:8080/code/data/foo.c is set to 1050 | 'text/plain; charset=ISO-8859-1' regardless of how the media type of a 1051 | .c file is defined in the system configuration files. 1052 | 1053 | 1054 | Downloads 1055 | --------- 1056 | The :meth:`ice.Ice.download` method may be used to force a client, e.g. 1057 | a browser, to prompt the user to save the returned content locally as a 1058 | file. 1059 | 1060 | .. automethod:: ice.Ice.download 1061 | :noindex: 1062 | 1063 | Here is an example. 1064 | 1065 | .. code:: python 1066 | 1067 | import ice 1068 | app = ice.cube() 1069 | 1070 | @app.get('/foo') 1071 | def foo(): 1072 | return app.download('hello, world', 'foo.txt') 1073 | 1074 | @app.get('/bar') 1075 | def bar(): 1076 | return app.download('hello, world', 'bar', 1077 | media_type='text/plain', charset='ISO-8859-1') 1078 | 1079 | if __name__ == '__main__': 1080 | app.run() 1081 | 1082 | The first argument to this method is the content to return, specified as 1083 | a string or sequence of bytes. The second argument is the filename that 1084 | the client should use to save the returned content. 1085 | 1086 | The discussion about media type and character set described in the 1087 | :ref:`static-files` applies to this section too. 1088 | 1089 | Visiting http://localhost:8080/foo with a standard browser displays a 1090 | prompt to download and save a file called foo.txt. Visiting 1091 | http://localhost:8080/bar displays a prompt to download and save a file 1092 | called bar. 1093 | 1094 | Since the first argument may be a sequence of bytes, it is quite simple 1095 | to return a static file for download. The :meth:`ice.Ice.static` method 1096 | usually returns a sequence of bytes which can be passed directly to the 1097 | :meth:`ice.Ice.download` method. The ``static()`` method may return an 1098 | HTTP status code, e.g. 403 or 404, which is handled gracefully by the 1099 | ``download()`` method in order to return an error page as response. 1100 | 1101 | .. code:: python 1102 | 1103 | import ice 1104 | app = ice.cube() 1105 | 1106 | @app.get('/code/<:path>') 1107 | def send_download(path): 1108 | return app.download(app.static('/var/www/project/code', path)) 1109 | 1110 | if __name__ == '__main__': 1111 | app.run() 1112 | 1113 | Note that in the above example, no filename argument is specified for 1114 | the ``download()`` method. The path argument that was specified in the 1115 | ``static()`` call is automatically used to obtain the filename for the 1116 | ``download()`` call. 1117 | 1118 | If there is a file called /var/www/project/code/data/foo.txt, then 1119 | visiting http://localhost:8080/code/data/foo.txt with a standard browser 1120 | displays a prompt to download and save a file called foo.txt. 1121 | 1122 | Here are the complete set of rules that determine the filename that is 1123 | used for the download. The rules are followed in the specified order. 1124 | 1125 | 1. If the *filename* argument is specified, the base name from this 1126 | argument, i.e. ``os.path.basename(filename)``, is used as the 1127 | filename for the download. 1128 | 2. If the *filename* argument is not specified, the base name from the 1129 | file path specified to a previous *static()* method call made while 1130 | handling the current request is used. 1131 | 3. If the *filename* argument is not specified and there was no 1132 | ``static()`` call made previously for the current request, then the 1133 | base name from the current HTTP request path is used. 1134 | 4. As a result of the above three steps, if the resultant *filename* 1135 | turns out to be empty, then ice.LogicError is raised. 1136 | 1137 | The first two points have been demonstrated in the previous two examples 1138 | above. The last two points are demonstrated in the following example. 1139 | 1140 | .. code:: python 1141 | 1142 | import ice 1143 | app = ice.cube() 1144 | 1145 | @app.get('/') 1146 | def send_download(): 1147 | return app.download('hello, world') 1148 | 1149 | if __name__ == '__main__': 1150 | app.run() 1151 | 1152 | Visiting http://localhost:8080/foo.txt with a standard browser would 1153 | download a file foo.txt. However, visiting http://localhost:8080/foo/ 1154 | would display an error due to the unhandled ice.LogicError that is 1155 | raised because no filename can be determined from the request path /foo/ 1156 | which refers to a directory, not a file. 1157 | 1158 | 1159 | Request Environ 1160 | --------------- 1161 | The following example shows how to access the ``environ`` dictionary 1162 | defined in the `WSGI specification`_. 1163 | 1164 | .. _WSGI specification: https://www.python.org/dev/peps/pep-3333/#environ-variables 1165 | 1166 | .. code:: python 1167 | 1168 | import ice 1169 | app = ice.cube() 1170 | 1171 | @app.get('/') 1172 | def foo(): 1173 | user_agent = app.request.environ.get('HTTP_USER_AGENT', None) 1174 | return ('' 1175 | '{}
'.format(user_agent)) 1177 | 1178 | app.run() 1179 | 1180 | The ``environ`` dictionary specified in the WSGI specification is made 1181 | available in ``app.request.environ``. The above example retrieves the 1182 | HTTP User-Agent header from this dictionary and displays it to the 1183 | client. 1184 | 1185 | 1186 | More 1187 | ---- 1188 | Since this is a microframework with a very limited set of features, 1189 | it is possible that you may find from time to time that this framework 1190 | is missing a useful API that another major framework provides. In such a 1191 | case, you have direct access to the WSGI internals to do what you want 1192 | via the documented API (see :ref:`api`). 1193 | 1194 | If you believe that the missing feature would be useful to all users of 1195 | this framework, please feel free to send a patch or a pull request. 1196 | --------------------------------------------------------------------------------