We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Inconsistent behavior between FileContentsManager and Contents Manager API docs
Discrepancies with [IPEP-27](https://github.com/ipython/ipython/wiki/IPEP-27:-Contents-Service#contents-api) noted while working on a custom subclass of `ContentsManager`:
- Where IPEP-27 lists `'modified'` as an expected dictionary key, `FileContentsManager` uses `'last_modified'` instead.
- IPEP-27 specifies that all three of `'contents'`, `'format'`, and `'mimetype'` will be null if `contents` is null.  In many (possibly all?) cases however, `'mimetype'` is non-null when `'contents` is null.

Cf. https://github.com/ipython/ipython/pull/7140.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `IPython/html/services/contents/handlers.py`
Content:
```
1 """Tornado handlers for the contents web service."""
2 
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 
6 import json
7 
8 from tornado import web
9 
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
12 
13 from IPython.html.base.handlers import (
14     IPythonHandler, json_errors, path_regex,
15 )
16 
17 
18 def sort_key(model):
19     """key function for case-insensitive sort by name and type"""
20     iname = model['name'].lower()
21     type_key = {
22         'directory' : '0',
23         'notebook'  : '1',
24         'file'      : '2',
25     }.get(model['type'], '9')
26     return u'%s%s' % (type_key, iname)
27 
28 class ContentsHandler(IPythonHandler):
29 
30     SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31 
32     def location_url(self, path):
33         """Return the full URL location of a file.
34 
35         Parameters
36         ----------
37         path : unicode
38             The API path of the file, such as "foo/bar.txt".
39         """
40         return url_escape(url_path_join(
41             self.base_url, 'api', 'contents', path
42         ))
43 
44     def _finish_model(self, model, location=True):
45         """Finish a JSON request with a model, setting relevant headers, etc."""
46         if location:
47             location = self.location_url(model['path'])
48             self.set_header('Location', location)
49         self.set_header('Last-Modified', model['last_modified'])
50         self.set_header('Content-Type', 'application/json')
51         self.finish(json.dumps(model, default=date_default))
52 
53     @web.authenticated
54     @json_errors
55     def get(self, path=''):
56         """Return a model for a file or directory.
57 
58         A directory model contains a list of models (without content)
59         of the files and directories it contains.
60         """
61         path = path or ''
62         type_ = self.get_query_argument('type', default=None)
63         if type_ not in {None, 'directory', 'file', 'notebook'}:
64             raise web.HTTPError(400, u'Type %r is invalid' % type_)
65 
66         format = self.get_query_argument('format', default=None)
67         if format not in {None, 'text', 'base64'}:
68             raise web.HTTPError(400, u'Format %r is invalid' % format)
69 
70         model = self.contents_manager.get(path=path, type_=type_, format=format)
71         if model['type'] == 'directory':
72             # group listing by type, then by name (case-insensitive)
73             # FIXME: sorting should be done in the frontends
74             model['content'].sort(key=sort_key)
75         self._finish_model(model, location=False)
76 
77     @web.authenticated
78     @json_errors
79     def patch(self, path=''):
80         """PATCH renames a file or directory without re-uploading content."""
81         cm = self.contents_manager
82         model = self.get_json_body()
83         if model is None:
84             raise web.HTTPError(400, u'JSON body missing')
85         model = cm.update(model, path)
86         self._finish_model(model)
87 
88     def _copy(self, copy_from, copy_to=None):
89         """Copy a file, optionally specifying a target directory."""
90         self.log.info(u"Copying {copy_from} to {copy_to}".format(
91             copy_from=copy_from,
92             copy_to=copy_to or '',
93         ))
94         model = self.contents_manager.copy(copy_from, copy_to)
95         self.set_status(201)
96         self._finish_model(model)
97 
98     def _upload(self, model, path):
99         """Handle upload of a new file to path"""
100         self.log.info(u"Uploading file to %s", path)
101         model = self.contents_manager.new(model, path)
102         self.set_status(201)
103         self._finish_model(model)
104         
105     def _new_untitled(self, path, type='', ext=''):
106         """Create a new, empty untitled entity"""
107         self.log.info(u"Creating new %s in %s", type or 'file', path)
108         model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
109         self.set_status(201)
110         self._finish_model(model)
111 
112     def _save(self, model, path):
113         """Save an existing file."""
114         self.log.info(u"Saving file at %s", path)
115         model = self.contents_manager.save(model, path)
116         self._finish_model(model)
117 
118     @web.authenticated
119     @json_errors
120     def post(self, path=''):
121         """Create a new file in the specified path.
122 
123         POST creates new files. The server always decides on the name.
124 
125         POST /api/contents/path
126           New untitled, empty file or directory.
127         POST /api/contents/path
128           with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
129           New copy of OtherNotebook in path
130         """
131 
132         cm = self.contents_manager
133 
134         if cm.file_exists(path):
135             raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
136 
137         if not cm.dir_exists(path):
138             raise web.HTTPError(404, "No such directory: %s" % path)
139 
140         model = self.get_json_body()
141 
142         if model is not None:
143             copy_from = model.get('copy_from')
144             ext = model.get('ext', '')
145             type = model.get('type', '')
146             if copy_from:
147                 self._copy(copy_from, path)
148             else:
149                 self._new_untitled(path, type=type, ext=ext)
150         else:
151             self._new_untitled(path)
152 
153     @web.authenticated
154     @json_errors
155     def put(self, path=''):
156         """Saves the file in the location specified by name and path.
157 
158         PUT is very similar to POST, but the requester specifies the name,
159         whereas with POST, the server picks the name.
160 
161         PUT /api/contents/path/Name.ipynb
162           Save notebook at ``path/Name.ipynb``. Notebook structure is specified
163           in `content` key of JSON request body. If content is not specified,
164           create a new empty notebook.
165         """
166         model = self.get_json_body()
167         if model:
168             if model.get('copy_from'):
169                 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
170             if self.contents_manager.file_exists(path):
171                 self._save(model, path)
172             else:
173                 self._upload(model, path)
174         else:
175             self._new_untitled(path)
176 
177     @web.authenticated
178     @json_errors
179     def delete(self, path=''):
180         """delete a file in the given path"""
181         cm = self.contents_manager
182         self.log.warn('delete %s', path)
183         cm.delete(path)
184         self.set_status(204)
185         self.finish()
186 
187 
188 class CheckpointsHandler(IPythonHandler):
189 
190     SUPPORTED_METHODS = ('GET', 'POST')
191 
192     @web.authenticated
193     @json_errors
194     def get(self, path=''):
195         """get lists checkpoints for a file"""
196         cm = self.contents_manager
197         checkpoints = cm.list_checkpoints(path)
198         data = json.dumps(checkpoints, default=date_default)
199         self.finish(data)
200 
201     @web.authenticated
202     @json_errors
203     def post(self, path=''):
204         """post creates a new checkpoint"""
205         cm = self.contents_manager
206         checkpoint = cm.create_checkpoint(path)
207         data = json.dumps(checkpoint, default=date_default)
208         location = url_path_join(self.base_url, 'api/contents',
209             path, 'checkpoints', checkpoint['id'])
210         self.set_header('Location', url_escape(location))
211         self.set_status(201)
212         self.finish(data)
213 
214 
215 class ModifyCheckpointsHandler(IPythonHandler):
216 
217     SUPPORTED_METHODS = ('POST', 'DELETE')
218 
219     @web.authenticated
220     @json_errors
221     def post(self, path, checkpoint_id):
222         """post restores a file from a checkpoint"""
223         cm = self.contents_manager
224         cm.restore_checkpoint(checkpoint_id, path)
225         self.set_status(204)
226         self.finish()
227 
228     @web.authenticated
229     @json_errors
230     def delete(self, path, checkpoint_id):
231         """delete clears a checkpoint for a given file"""
232         cm = self.contents_manager
233         cm.delete_checkpoint(checkpoint_id, path)
234         self.set_status(204)
235         self.finish()
236 
237 
238 class NotebooksRedirectHandler(IPythonHandler):
239     """Redirect /api/notebooks to /api/contents"""
240     SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
241 
242     def get(self, path):
243         self.log.warn("/api/notebooks is deprecated, use /api/contents")
244         self.redirect(url_path_join(
245             self.base_url,
246             'api/contents',
247             path
248         ))
249 
250     put = patch = post = delete = get
251 
252 
253 #-----------------------------------------------------------------------------
254 # URL to handler mappings
255 #-----------------------------------------------------------------------------
256 
257 
258 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
259 
260 default_handlers = [
261     (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
262     (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
263         ModifyCheckpointsHandler),
264     (r"/api/contents%s" % path_regex, ContentsHandler),
265     (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
266 ]
267
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py
--- a/IPython/html/services/contents/handlers.py
+++ b/IPython/html/services/contents/handlers.py
@@ -25,6 +25,56 @@
     }.get(model['type'], '9')
     return u'%s%s' % (type_key, iname)
 
+
+def validate_model(model, expect_content):
+    """
+    Validate a model returned by a ContentsManager method.
+
+    If expect_content is True, then we expect non-null entries for 'content'
+    and 'format'.
+    """
+    required_keys = {
+        "name",
+        "path",
+        "type",
+        "writable",
+        "created",
+        "last_modified",
+        "mimetype",
+        "content",
+        "format",
+    }
+    missing = required_keys - set(model.keys())
+    if missing:
+        raise web.HTTPError(
+            500,
+            u"Missing Model Keys: {missing}".format(missing=missing),
+        )
+
+    maybe_none_keys = ['content', 'format']
+    if model['type'] == 'file':
+        # mimetype should be populated only for file models
+        maybe_none_keys.append('mimetype')
+    if expect_content:
+        errors = [key for key in maybe_none_keys if model[key] is None]
+        if errors:
+            raise web.HTTPError(
+                500,
+                u"Keys unexpectedly None: {keys}".format(keys=errors),
+            )
+    else:
+        errors = {
+            key: model[key]
+            for key in maybe_none_keys
+            if model[key] is not None
+        }
+        if errors:
+            raise web.HTTPError(
+                500,
+                u"Keys unexpectedly not None: {keys}".format(keys=errors),
+            )
+
+
 class ContentsHandler(IPythonHandler):
 
     SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
@@ -72,6 +122,7 @@
             # group listing by type, then by name (case-insensitive)
             # FIXME: sorting should be done in the frontends
             model['content'].sort(key=sort_key)
+        validate_model(model, expect_content=True)
         self._finish_model(model, location=False)
 
     @web.authenticated
@@ -83,6 +134,7 @@
         if model is None:
             raise web.HTTPError(400, u'JSON body missing')
         model = cm.update(model, path)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _copy(self, copy_from, copy_to=None):
@@ -93,6 +145,7 @@
         ))
         model = self.contents_manager.copy(copy_from, copy_to)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _upload(self, model, path):
@@ -100,6 +153,7 @@
         self.log.info(u"Uploading file to %s", path)
         model = self.contents_manager.new(model, path)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
         
     def _new_untitled(self, path, type='', ext=''):
@@ -107,12 +161,14 @@
         self.log.info(u"Creating new %s in %s", type or 'file', path)
         model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _save(self, model, path):
         """Save an existing file."""
         self.log.info(u"Saving file at %s", path)
         model = self.contents_manager.save(model, path)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     @web.authenticated

{"golden_diff": "diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py\n--- a/IPython/html/services/contents/handlers.py\n+++ b/IPython/html/services/contents/handlers.py\n@@ -25,6 +25,56 @@\n     }.get(model['type'], '9')\n     return u'%s%s' % (type_key, iname)\n \n+\n+def validate_model(model, expect_content):\n+    \"\"\"\n+    Validate a model returned by a ContentsManager method.\n+\n+    If expect_content is True, then we expect non-null entries for 'content'\n+    and 'format'.\n+    \"\"\"\n+    required_keys = {\n+        \"name\",\n+        \"path\",\n+        \"type\",\n+        \"writable\",\n+        \"created\",\n+        \"last_modified\",\n+        \"mimetype\",\n+        \"content\",\n+        \"format\",\n+    }\n+    missing = required_keys - set(model.keys())\n+    if missing:\n+        raise web.HTTPError(\n+            500,\n+            u\"Missing Model Keys: {missing}\".format(missing=missing),\n+        )\n+\n+    maybe_none_keys = ['content', 'format']\n+    if model['type'] == 'file':\n+        # mimetype should be populated only for file models\n+        maybe_none_keys.append('mimetype')\n+    if expect_content:\n+        errors = [key for key in maybe_none_keys if model[key] is None]\n+        if errors:\n+            raise web.HTTPError(\n+                500,\n+                u\"Keys unexpectedly None: {keys}\".format(keys=errors),\n+            )\n+    else:\n+        errors = {\n+            key: model[key]\n+            for key in maybe_none_keys\n+            if model[key] is not None\n+        }\n+        if errors:\n+            raise web.HTTPError(\n+                500,\n+                u\"Keys unexpectedly not None: {keys}\".format(keys=errors),\n+            )\n+\n+\n class ContentsHandler(IPythonHandler):\n \n     SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')\n@@ -72,6 +122,7 @@\n             # group listing by type, then by name (case-insensitive)\n             # FIXME: sorting should be done in the frontends\n             model['content'].sort(key=sort_key)\n+        validate_model(model, expect_content=True)\n         self._finish_model(model, location=False)\n \n     @web.authenticated\n@@ -83,6 +134,7 @@\n         if model is None:\n             raise web.HTTPError(400, u'JSON body missing')\n         model = cm.update(model, path)\n+        validate_model(model, expect_content=False)\n         self._finish_model(model)\n \n     def _copy(self, copy_from, copy_to=None):\n@@ -93,6 +145,7 @@\n         ))\n         model = self.contents_manager.copy(copy_from, copy_to)\n         self.set_status(201)\n+        validate_model(model, expect_content=False)\n         self._finish_model(model)\n \n     def _upload(self, model, path):\n@@ -100,6 +153,7 @@\n         self.log.info(u\"Uploading file to %s\", path)\n         model = self.contents_manager.new(model, path)\n         self.set_status(201)\n+        validate_model(model, expect_content=False)\n         self._finish_model(model)\n         \n     def _new_untitled(self, path, type='', ext=''):\n@@ -107,12 +161,14 @@\n         self.log.info(u\"Creating new %s in %s\", type or 'file', path)\n         model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)\n         self.set_status(201)\n+        validate_model(model, expect_content=False)\n         self._finish_model(model)\n \n     def _save(self, model, path):\n         \"\"\"Save an existing file.\"\"\"\n         self.log.info(u\"Saving file at %s\", path)\n         model = self.contents_manager.save(model, path)\n+        validate_model(model, expect_content=False)\n         self._finish_model(model)\n \n     @web.authenticated\n", "issue": "Inconsistent behavior between FileContentsManager and Contents Manager API docs\nDiscrepancies with [IPEP-27](https://github.com/ipython/ipython/wiki/IPEP-27:-Contents-Service#contents-api) noted while working on a custom subclass of `ContentsManager`:\n- Where IPEP-27 lists `'modified'` as an expected dictionary key, `FileContentsManager` uses `'last_modified'` instead.\n- IPEP-27 specifies that all three of `'contents'`, `'format'`, and `'mimetype'` will be null if `contents` is null.  In many (possibly all?) cases however, `'mimetype'` is non-null when `'contents` is null.\n\nCf. https://github.com/ipython/ipython/pull/7140.\n\n", "before_files": [{"content": "\"\"\"Tornado handlers for the contents web service.\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nimport json\n\nfrom tornado import web\n\nfrom IPython.html.utils import url_path_join, url_escape\nfrom IPython.utils.jsonutil import date_default\n\nfrom IPython.html.base.handlers import (\n    IPythonHandler, json_errors, path_regex,\n)\n\n\ndef sort_key(model):\n    \"\"\"key function for case-insensitive sort by name and type\"\"\"\n    iname = model['name'].lower()\n    type_key = {\n        'directory' : '0',\n        'notebook'  : '1',\n        'file'      : '2',\n    }.get(model['type'], '9')\n    return u'%s%s' % (type_key, iname)\n\nclass ContentsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')\n\n    def location_url(self, path):\n        \"\"\"Return the full URL location of a file.\n\n        Parameters\n        ----------\n        path : unicode\n            The API path of the file, such as \"foo/bar.txt\".\n        \"\"\"\n        return url_escape(url_path_join(\n            self.base_url, 'api', 'contents', path\n        ))\n\n    def _finish_model(self, model, location=True):\n        \"\"\"Finish a JSON request with a model, setting relevant headers, etc.\"\"\"\n        if location:\n            location = self.location_url(model['path'])\n            self.set_header('Location', location)\n        self.set_header('Last-Modified', model['last_modified'])\n        self.set_header('Content-Type', 'application/json')\n        self.finish(json.dumps(model, default=date_default))\n\n    @web.authenticated\n    @json_errors\n    def get(self, path=''):\n        \"\"\"Return a model for a file or directory.\n\n        A directory model contains a list of models (without content)\n        of the files and directories it contains.\n        \"\"\"\n        path = path or ''\n        type_ = self.get_query_argument('type', default=None)\n        if type_ not in {None, 'directory', 'file', 'notebook'}:\n            raise web.HTTPError(400, u'Type %r is invalid' % type_)\n\n        format = self.get_query_argument('format', default=None)\n        if format not in {None, 'text', 'base64'}:\n            raise web.HTTPError(400, u'Format %r is invalid' % format)\n\n        model = self.contents_manager.get(path=path, type_=type_, format=format)\n        if model['type'] == 'directory':\n            # group listing by type, then by name (case-insensitive)\n            # FIXME: sorting should be done in the frontends\n            model['content'].sort(key=sort_key)\n        self._finish_model(model, location=False)\n\n    @web.authenticated\n    @json_errors\n    def patch(self, path=''):\n        \"\"\"PATCH renames a file or directory without re-uploading content.\"\"\"\n        cm = self.contents_manager\n        model = self.get_json_body()\n        if model is None:\n            raise web.HTTPError(400, u'JSON body missing')\n        model = cm.update(model, path)\n        self._finish_model(model)\n\n    def _copy(self, copy_from, copy_to=None):\n        \"\"\"Copy a file, optionally specifying a target directory.\"\"\"\n        self.log.info(u\"Copying {copy_from} to {copy_to}\".format(\n            copy_from=copy_from,\n            copy_to=copy_to or '',\n        ))\n        model = self.contents_manager.copy(copy_from, copy_to)\n        self.set_status(201)\n        self._finish_model(model)\n\n    def _upload(self, model, path):\n        \"\"\"Handle upload of a new file to path\"\"\"\n        self.log.info(u\"Uploading file to %s\", path)\n        model = self.contents_manager.new(model, path)\n        self.set_status(201)\n        self._finish_model(model)\n        \n    def _new_untitled(self, path, type='', ext=''):\n        \"\"\"Create a new, empty untitled entity\"\"\"\n        self.log.info(u\"Creating new %s in %s\", type or 'file', path)\n        model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)\n        self.set_status(201)\n        self._finish_model(model)\n\n    def _save(self, model, path):\n        \"\"\"Save an existing file.\"\"\"\n        self.log.info(u\"Saving file at %s\", path)\n        model = self.contents_manager.save(model, path)\n        self._finish_model(model)\n\n    @web.authenticated\n    @json_errors\n    def post(self, path=''):\n        \"\"\"Create a new file in the specified path.\n\n        POST creates new files. The server always decides on the name.\n\n        POST /api/contents/path\n          New untitled, empty file or directory.\n        POST /api/contents/path\n          with body {\"copy_from\" : \"/path/to/OtherNotebook.ipynb\"}\n          New copy of OtherNotebook in path\n        \"\"\"\n\n        cm = self.contents_manager\n\n        if cm.file_exists(path):\n            raise web.HTTPError(400, \"Cannot POST to files, use PUT instead.\")\n\n        if not cm.dir_exists(path):\n            raise web.HTTPError(404, \"No such directory: %s\" % path)\n\n        model = self.get_json_body()\n\n        if model is not None:\n            copy_from = model.get('copy_from')\n            ext = model.get('ext', '')\n            type = model.get('type', '')\n            if copy_from:\n                self._copy(copy_from, path)\n            else:\n                self._new_untitled(path, type=type, ext=ext)\n        else:\n            self._new_untitled(path)\n\n    @web.authenticated\n    @json_errors\n    def put(self, path=''):\n        \"\"\"Saves the file in the location specified by name and path.\n\n        PUT is very similar to POST, but the requester specifies the name,\n        whereas with POST, the server picks the name.\n\n        PUT /api/contents/path/Name.ipynb\n          Save notebook at ``path/Name.ipynb``. Notebook structure is specified\n          in `content` key of JSON request body. If content is not specified,\n          create a new empty notebook.\n        \"\"\"\n        model = self.get_json_body()\n        if model:\n            if model.get('copy_from'):\n                raise web.HTTPError(400, \"Cannot copy with PUT, only POST\")\n            if self.contents_manager.file_exists(path):\n                self._save(model, path)\n            else:\n                self._upload(model, path)\n        else:\n            self._new_untitled(path)\n\n    @web.authenticated\n    @json_errors\n    def delete(self, path=''):\n        \"\"\"delete a file in the given path\"\"\"\n        cm = self.contents_manager\n        self.log.warn('delete %s', path)\n        cm.delete(path)\n        self.set_status(204)\n        self.finish()\n\n\nclass CheckpointsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = ('GET', 'POST')\n\n    @web.authenticated\n    @json_errors\n    def get(self, path=''):\n        \"\"\"get lists checkpoints for a file\"\"\"\n        cm = self.contents_manager\n        checkpoints = cm.list_checkpoints(path)\n        data = json.dumps(checkpoints, default=date_default)\n        self.finish(data)\n\n    @web.authenticated\n    @json_errors\n    def post(self, path=''):\n        \"\"\"post creates a new checkpoint\"\"\"\n        cm = self.contents_manager\n        checkpoint = cm.create_checkpoint(path)\n        data = json.dumps(checkpoint, default=date_default)\n        location = url_path_join(self.base_url, 'api/contents',\n            path, 'checkpoints', checkpoint['id'])\n        self.set_header('Location', url_escape(location))\n        self.set_status(201)\n        self.finish(data)\n\n\nclass ModifyCheckpointsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = ('POST', 'DELETE')\n\n    @web.authenticated\n    @json_errors\n    def post(self, path, checkpoint_id):\n        \"\"\"post restores a file from a checkpoint\"\"\"\n        cm = self.contents_manager\n        cm.restore_checkpoint(checkpoint_id, path)\n        self.set_status(204)\n        self.finish()\n\n    @web.authenticated\n    @json_errors\n    def delete(self, path, checkpoint_id):\n        \"\"\"delete clears a checkpoint for a given file\"\"\"\n        cm = self.contents_manager\n        cm.delete_checkpoint(checkpoint_id, path)\n        self.set_status(204)\n        self.finish()\n\n\nclass NotebooksRedirectHandler(IPythonHandler):\n    \"\"\"Redirect /api/notebooks to /api/contents\"\"\"\n    SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')\n\n    def get(self, path):\n        self.log.warn(\"/api/notebooks is deprecated, use /api/contents\")\n        self.redirect(url_path_join(\n            self.base_url,\n            'api/contents',\n            path\n        ))\n\n    put = patch = post = delete = get\n\n\n#-----------------------------------------------------------------------------\n# URL to handler mappings\n#-----------------------------------------------------------------------------\n\n\n_checkpoint_id_regex = r\"(?P<checkpoint_id>[\\w-]+)\"\n\ndefault_handlers = [\n    (r\"/api/contents%s/checkpoints\" % path_regex, CheckpointsHandler),\n    (r\"/api/contents%s/checkpoints/%s\" % (path_regex, _checkpoint_id_regex),\n        ModifyCheckpointsHandler),\n    (r\"/api/contents%s\" % path_regex, ContentsHandler),\n    (r\"/api/notebooks/?(.*)\", NotebooksRedirectHandler),\n]\n", "path": "IPython/html/services/contents/handlers.py"}], "after_files": [{"content": "\"\"\"Tornado handlers for the contents web service.\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nimport json\n\nfrom tornado import web\n\nfrom IPython.html.utils import url_path_join, url_escape\nfrom IPython.utils.jsonutil import date_default\n\nfrom IPython.html.base.handlers import (\n    IPythonHandler, json_errors, path_regex,\n)\n\n\ndef sort_key(model):\n    \"\"\"key function for case-insensitive sort by name and type\"\"\"\n    iname = model['name'].lower()\n    type_key = {\n        'directory' : '0',\n        'notebook'  : '1',\n        'file'      : '2',\n    }.get(model['type'], '9')\n    return u'%s%s' % (type_key, iname)\n\n\ndef validate_model(model, expect_content):\n    \"\"\"\n    Validate a model returned by a ContentsManager method.\n\n    If expect_content is True, then we expect non-null entries for 'content'\n    and 'format'.\n    \"\"\"\n    required_keys = {\n        \"name\",\n        \"path\",\n        \"type\",\n        \"writable\",\n        \"created\",\n        \"last_modified\",\n        \"mimetype\",\n        \"content\",\n        \"format\",\n    }\n    missing = required_keys - set(model.keys())\n    if missing:\n        raise web.HTTPError(\n            500,\n            u\"Missing Model Keys: {missing}\".format(missing=missing),\n        )\n\n    maybe_none_keys = ['content', 'format']\n    if model['type'] == 'file':\n        # mimetype should be populated only for file models\n        maybe_none_keys.append('mimetype')\n    if expect_content:\n        errors = [key for key in maybe_none_keys if model[key] is None]\n        if errors:\n            raise web.HTTPError(\n                500,\n                u\"Keys unexpectedly None: {keys}\".format(keys=errors),\n            )\n    else:\n        errors = {\n            key: model[key]\n            for key in maybe_none_keys\n            if model[key] is not None\n        }\n        if errors:\n            raise web.HTTPError(\n                500,\n                u\"Keys unexpectedly not None: {keys}\".format(keys=errors),\n            )\n\n\nclass ContentsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')\n\n    def location_url(self, path):\n        \"\"\"Return the full URL location of a file.\n\n        Parameters\n        ----------\n        path : unicode\n            The API path of the file, such as \"foo/bar.txt\".\n        \"\"\"\n        return url_escape(url_path_join(\n            self.base_url, 'api', 'contents', path\n        ))\n\n    def _finish_model(self, model, location=True):\n        \"\"\"Finish a JSON request with a model, setting relevant headers, etc.\"\"\"\n        if location:\n            location = self.location_url(model['path'])\n            self.set_header('Location', location)\n        self.set_header('Last-Modified', model['last_modified'])\n        self.set_header('Content-Type', 'application/json')\n        self.finish(json.dumps(model, default=date_default))\n\n    @web.authenticated\n    @json_errors\n    def get(self, path=''):\n        \"\"\"Return a model for a file or directory.\n\n        A directory model contains a list of models (without content)\n        of the files and directories it contains.\n        \"\"\"\n        path = path or ''\n        type_ = self.get_query_argument('type', default=None)\n        if type_ not in {None, 'directory', 'file', 'notebook'}:\n            raise web.HTTPError(400, u'Type %r is invalid' % type_)\n\n        format = self.get_query_argument('format', default=None)\n        if format not in {None, 'text', 'base64'}:\n            raise web.HTTPError(400, u'Format %r is invalid' % format)\n\n        model = self.contents_manager.get(path=path, type_=type_, format=format)\n        if model['type'] == 'directory':\n            # group listing by type, then by name (case-insensitive)\n            # FIXME: sorting should be done in the frontends\n            model['content'].sort(key=sort_key)\n        validate_model(model, expect_content=True)\n        self._finish_model(model, location=False)\n\n    @web.authenticated\n    @json_errors\n    def patch(self, path=''):\n        \"\"\"PATCH renames a file or directory without re-uploading content.\"\"\"\n        cm = self.contents_manager\n        model = self.get_json_body()\n        if model is None:\n            raise web.HTTPError(400, u'JSON body missing')\n        model = cm.update(model, path)\n        validate_model(model, expect_content=False)\n        self._finish_model(model)\n\n    def _copy(self, copy_from, copy_to=None):\n        \"\"\"Copy a file, optionally specifying a target directory.\"\"\"\n        self.log.info(u\"Copying {copy_from} to {copy_to}\".format(\n            copy_from=copy_from,\n            copy_to=copy_to or '',\n        ))\n        model = self.contents_manager.copy(copy_from, copy_to)\n        self.set_status(201)\n        validate_model(model, expect_content=False)\n        self._finish_model(model)\n\n    def _upload(self, model, path):\n        \"\"\"Handle upload of a new file to path\"\"\"\n        self.log.info(u\"Uploading file to %s\", path)\n        model = self.contents_manager.new(model, path)\n        self.set_status(201)\n        validate_model(model, expect_content=False)\n        self._finish_model(model)\n        \n    def _new_untitled(self, path, type='', ext=''):\n        \"\"\"Create a new, empty untitled entity\"\"\"\n        self.log.info(u\"Creating new %s in %s\", type or 'file', path)\n        model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)\n        self.set_status(201)\n        validate_model(model, expect_content=False)\n        self._finish_model(model)\n\n    def _save(self, model, path):\n        \"\"\"Save an existing file.\"\"\"\n        self.log.info(u\"Saving file at %s\", path)\n        model = self.contents_manager.save(model, path)\n        validate_model(model, expect_content=False)\n        self._finish_model(model)\n\n    @web.authenticated\n    @json_errors\n    def post(self, path=''):\n        \"\"\"Create a new file in the specified path.\n\n        POST creates new files. The server always decides on the name.\n\n        POST /api/contents/path\n          New untitled, empty file or directory.\n        POST /api/contents/path\n          with body {\"copy_from\" : \"/path/to/OtherNotebook.ipynb\"}\n          New copy of OtherNotebook in path\n        \"\"\"\n\n        cm = self.contents_manager\n\n        if cm.file_exists(path):\n            raise web.HTTPError(400, \"Cannot POST to files, use PUT instead.\")\n\n        if not cm.dir_exists(path):\n            raise web.HTTPError(404, \"No such directory: %s\" % path)\n\n        model = self.get_json_body()\n\n        if model is not None:\n            copy_from = model.get('copy_from')\n            ext = model.get('ext', '')\n            type = model.get('type', '')\n            if copy_from:\n                self._copy(copy_from, path)\n            else:\n                self._new_untitled(path, type=type, ext=ext)\n        else:\n            self._new_untitled(path)\n\n    @web.authenticated\n    @json_errors\n    def put(self, path=''):\n        \"\"\"Saves the file in the location specified by name and path.\n\n        PUT is very similar to POST, but the requester specifies the name,\n        whereas with POST, the server picks the name.\n\n        PUT /api/contents/path/Name.ipynb\n          Save notebook at ``path/Name.ipynb``. Notebook structure is specified\n          in `content` key of JSON request body. If content is not specified,\n          create a new empty notebook.\n        \"\"\"\n        model = self.get_json_body()\n        if model:\n            if model.get('copy_from'):\n                raise web.HTTPError(400, \"Cannot copy with PUT, only POST\")\n            if self.contents_manager.file_exists(path):\n                self._save(model, path)\n            else:\n                self._upload(model, path)\n        else:\n            self._new_untitled(path)\n\n    @web.authenticated\n    @json_errors\n    def delete(self, path=''):\n        \"\"\"delete a file in the given path\"\"\"\n        cm = self.contents_manager\n        self.log.warn('delete %s', path)\n        cm.delete(path)\n        self.set_status(204)\n        self.finish()\n\n\nclass CheckpointsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = ('GET', 'POST')\n\n    @web.authenticated\n    @json_errors\n    def get(self, path=''):\n        \"\"\"get lists checkpoints for a file\"\"\"\n        cm = self.contents_manager\n        checkpoints = cm.list_checkpoints(path)\n        data = json.dumps(checkpoints, default=date_default)\n        self.finish(data)\n\n    @web.authenticated\n    @json_errors\n    def post(self, path=''):\n        \"\"\"post creates a new checkpoint\"\"\"\n        cm = self.contents_manager\n        checkpoint = cm.create_checkpoint(path)\n        data = json.dumps(checkpoint, default=date_default)\n        location = url_path_join(self.base_url, 'api/contents',\n            path, 'checkpoints', checkpoint['id'])\n        self.set_header('Location', url_escape(location))\n        self.set_status(201)\n        self.finish(data)\n\n\nclass ModifyCheckpointsHandler(IPythonHandler):\n\n    SUPPORTED_METHODS = ('POST', 'DELETE')\n\n    @web.authenticated\n    @json_errors\n    def post(self, path, checkpoint_id):\n        \"\"\"post restores a file from a checkpoint\"\"\"\n        cm = self.contents_manager\n        cm.restore_checkpoint(checkpoint_id, path)\n        self.set_status(204)\n        self.finish()\n\n    @web.authenticated\n    @json_errors\n    def delete(self, path, checkpoint_id):\n        \"\"\"delete clears a checkpoint for a given file\"\"\"\n        cm = self.contents_manager\n        cm.delete_checkpoint(checkpoint_id, path)\n        self.set_status(204)\n        self.finish()\n\n\nclass NotebooksRedirectHandler(IPythonHandler):\n    \"\"\"Redirect /api/notebooks to /api/contents\"\"\"\n    SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')\n\n    def get(self, path):\n        self.log.warn(\"/api/notebooks is deprecated, use /api/contents\")\n        self.redirect(url_path_join(\n            self.base_url,\n            'api/contents',\n            path\n        ))\n\n    put = patch = post = delete = get\n\n\n#-----------------------------------------------------------------------------\n# URL to handler mappings\n#-----------------------------------------------------------------------------\n\n\n_checkpoint_id_regex = r\"(?P<checkpoint_id>[\\w-]+)\"\n\ndefault_handlers = [\n    (r\"/api/contents%s/checkpoints\" % path_regex, CheckpointsHandler),\n    (r\"/api/contents%s/checkpoints/%s\" % (path_regex, _checkpoint_id_regex),\n        ModifyCheckpointsHandler),\n    (r\"/api/contents%s\" % path_regex, ContentsHandler),\n    (r\"/api/notebooks/?(.*)\", NotebooksRedirectHandler),\n]\n", "path": "IPython/html/services/contents/handlers.py"}]}