Merge lp:~allenap/maas/headers-optional into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 1171
Proposed branch: lp:~allenap/maas/headers-optional
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 211 lines (+129/-22)
2 files modified
src/maascli/api.py (+69/-22)
src/maascli/tests/test_api.py (+60/-0)
To merge this branch: bzr merge lp:~allenap/maas/headers-optional
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+128075@code.launchpad.net

Commit message

maas-cli only shows the response line and headers when the -d/--debug flag is used.

Previously these were always shown. Hiding them is important so that output from maas-cli can be usefully piped to other programs.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maascli/api.py'
2--- src/maascli/api.py 2012-10-02 12:39:54 +0000
3+++ src/maascli/api.py 2012-10-04 20:07:26 +0000
4@@ -14,6 +14,7 @@
5 "register",
6 ]
7
8+from email.message import Message
9 from getpass import getpass
10 import httplib
11 from itertools import chain
12@@ -87,6 +88,51 @@
13 return json.loads(content)
14
15
16+def get_response_content_type(response):
17+ """Returns the response's content-type, without parameters.
18+
19+ If the content-type was not set in the response, returns `None`.
20+
21+ :type response: :class:`httplib2.Response`
22+ """
23+ try:
24+ content_type = response["content-type"]
25+ except KeyError:
26+ return None
27+ else:
28+ # It seems odd to create a Message instance here, but at the time of
29+ # writing it's the only place that has the smarts to correctly deal
30+ # with a Content-Type that contains a charset (or other parameters).
31+ message = Message()
32+ message.set_type(content_type)
33+ return message.get_content_type()
34+
35+
36+def is_response_textual(response):
37+ """Is the response body text?"""
38+ content_type = get_response_content_type(response)
39+ return (
40+ content_type.endswith("/json") or
41+ content_type.startswith("text/"))
42+
43+
44+def print_headers(headers, file=sys.stdout):
45+ """Show an HTTP response in a human-friendly way.
46+
47+ :type headers: :class:`httplib2.Response`, or :class:`dict`
48+ """
49+ # Function to change headers like "transfer-encoding" into
50+ # "Transfer-Encoding".
51+ cap = lambda header: "-".join(
52+ part.capitalize() for part in header.split("-"))
53+ # Format string to prettify reporting of response headers.
54+ form = "%%%ds: %%s" % (
55+ max(len(header) for header in headers) + 2)
56+ # Print the response.
57+ for header in sorted(headers):
58+ print(form % (cap(header), headers[header]), file=file)
59+
60+
61 class cmd_login(Command):
62 """Log-in to a remote API, storing its description and credentials.
63
64@@ -203,6 +249,9 @@
65 parser.add_argument(param)
66 parser.add_argument(
67 "data", type=self.name_value_pair, nargs="*")
68+ parser.add_argument(
69+ "-d", "--debug", action="store_true", default=False,
70+ help="Display more information about API responses.")
71
72 def __call__(self, options):
73 # TODO: this is el-cheapo URI Template
74@@ -226,8 +275,11 @@
75 response, content = http.request(
76 uri, self.method, body=body, headers=headers)
77
78- # TODO: decide on how to display responses to users.
79- self.print_response(response, content)
80+ # Output.
81+ if options.debug:
82+ self.print_debug(response)
83+ self.print_response(
84+ response, content, options.debug)
85
86 # 2xx status codes are all okay.
87 if response.status // 100 != 2:
88@@ -276,29 +328,24 @@
89 auth = MAASOAuth(*credentials)
90 auth.sign_request(uri, headers)
91
92+ @staticmethod
93+ def print_debug(response):
94+ """Dump the response line and headers to stderr."""
95+ print(response.status, response.reason, file=sys.stderr)
96+ print(file=sys.stderr)
97+ print_headers(response, file=sys.stderr)
98+ print(file=sys.stderr)
99+
100 @classmethod
101 def print_response(cls, response, content):
102- """Show an HTTP response in a human-friendly way."""
103- # Print the response.
104- print(response.status, response.reason)
105- print()
106- cls.print_headers(response)
107- print()
108- print(content)
109+ """Print the response body if it's textual.
110
111- @staticmethod
112- def print_headers(headers):
113- """Show an HTTP response in a human-friendly way."""
114- # Function to change headers like "transfer-encoding" into
115- # "Transfer-Encoding".
116- cap = lambda header: "-".join(
117- part.capitalize() for part in header.split("-"))
118- # Format string to prettify reporting of response headers.
119- form = "%%%ds: %%s" % (
120- max(len(header) for header in headers) + 2)
121- # Print the response.
122- for header in sorted(headers):
123- print(form % (cap(header), headers[header]))
124+ Otherwise write it raw to stdout.
125+ """
126+ if is_response_textual(response):
127+ print(content) # Trailing newline, might encode.
128+ else:
129+ sys.stdout.write(content) # Raw, binary.
130
131
132 def register_actions(profile, handler, parser):
133
134=== modified file 'src/maascli/tests/test_api.py'
135--- src/maascli/tests/test_api.py 2012-10-02 12:56:40 +0000
136+++ src/maascli/tests/test_api.py 2012-10-04 20:07:26 +0000
137@@ -12,7 +12,9 @@
138 __metaclass__ = type
139 __all__ = []
140
141+import collections
142 import httplib
143+import io
144 import json
145 import sys
146
147@@ -127,6 +129,64 @@
148 "Expected application/json, got: text/css",
149 "%s" % error)
150
151+ def test_get_response_content_type_returns_content_type_header(self):
152+ response = httplib2.Response(
153+ {"content-type": "application/json"})
154+ self.assertEqual(
155+ "application/json",
156+ api.get_response_content_type(response))
157+
158+ def test_get_response_content_type_omits_parameters(self):
159+ response = httplib2.Response(
160+ {"content-type": "application/json; charset=utf-8"})
161+ self.assertEqual(
162+ "application/json",
163+ api.get_response_content_type(response))
164+
165+ def test_get_response_content_type_return_None_when_type_not_found(self):
166+ response = httplib2.Response({})
167+ self.assertIsNone(api.get_response_content_type(response))
168+
169+ def test_print_headers(self):
170+ # print_headers() prints the given headers, in order, with each
171+ # hyphen-delimited part of the header name capitalised, to the given
172+ # file, with the names right aligned, and with a 2 space left margin.
173+ headers = collections.OrderedDict()
174+ headers["two-two"] = factory.make_name("two")
175+ headers["one"] = factory.make_name("one")
176+ buf = io.StringIO()
177+ api.print_headers(headers, buf)
178+ self.assertEqual(
179+ (" One: %(one)s\n"
180+ " Two-Two: %(two-two)s\n") % headers,
181+ buf.getvalue())
182+
183+
184+class TestIsResponseTextual(TestCase):
185+ """Tests for `is_response_textual`."""
186+
187+ content_types_textual_map = {
188+ "text/plain": True,
189+ "text/yaml": True,
190+ "text/foobar": True,
191+ "application/json": True,
192+ "image/png": False,
193+ "video/webm": False,
194+ }
195+
196+ scenarios = sorted(
197+ (ctype, {"content_type": ctype, "is_textual": is_textual})
198+ for ctype, is_textual in content_types_textual_map.items()
199+ )
200+
201+ def test_type(self):
202+ grct = self.patch(api, "get_response_content_type")
203+ grct.return_value = self.content_type
204+ self.assertEqual(
205+ self.is_textual,
206+ api.is_response_textual(sentinel.response))
207+ grct.assert_called_once_with(sentinel.response)
208+
209
210 class TestAction(TestCase):
211 """Tests for :class:`maascli.api.Action`."""