sphinxcontrib-httpexample 2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sphinxcontrib/httpexample/__init__.py +40 -0
- sphinxcontrib/httpexample/builders.py +277 -0
- sphinxcontrib/httpexample/directives.py +239 -0
- sphinxcontrib/httpexample/lexers.py +92 -0
- sphinxcontrib/httpexample/parsers.py +119 -0
- sphinxcontrib/httpexample/static/sphinxcontrib-httpexample.css +30 -0
- sphinxcontrib/httpexample/static/sphinxcontrib-httpexample.js +99 -0
- sphinxcontrib/httpexample/utils.py +107 -0
- sphinxcontrib_httpexample-2.0.dist-info/METADATA +142 -0
- sphinxcontrib_httpexample-2.0.dist-info/RECORD +11 -0
- sphinxcontrib_httpexample-2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from importlib import metadata
|
|
3
|
+
from sphinxcontrib.httpexample.directives import HTTPExample
|
|
4
|
+
from sphinxcontrib.httpexample.directives import HTTPExampleBlock
|
|
5
|
+
from sphinxcontrib.httpexample.directives import register_builder
|
|
6
|
+
from sphinxcontrib.httpexample.lexers import HTTPResponseLexer
|
|
7
|
+
from sphinxcontrib.httpexample.parsers import HTTPRequest
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CSS_FILE = "sphinxcontrib-httpexample.css"
|
|
13
|
+
JS_FILE = "sphinxcontrib-httpexample.js"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def copy_assets(app, exception):
|
|
17
|
+
if app.builder.name != "html" or exception:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# CSS
|
|
21
|
+
src = os.path.join(os.path.dirname(__file__), "static", CSS_FILE)
|
|
22
|
+
dst = os.path.join(app.builder.outdir, "_static", CSS_FILE)
|
|
23
|
+
shutil.copyfile(src, dst)
|
|
24
|
+
|
|
25
|
+
# JS
|
|
26
|
+
src = os.path.join(os.path.dirname(__file__), "static", JS_FILE)
|
|
27
|
+
dst = os.path.join(app.builder.outdir, "_static", JS_FILE)
|
|
28
|
+
shutil.copyfile(src, dst)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def setup(app):
|
|
32
|
+
app.connect("build-finished", copy_assets)
|
|
33
|
+
app.add_directive_to_domain("http", "example", HTTPExample)
|
|
34
|
+
app.add_directive_to_domain("http", "example-block", HTTPExampleBlock)
|
|
35
|
+
app.add_js_file(JS_FILE)
|
|
36
|
+
app.add_css_file(CSS_FILE)
|
|
37
|
+
app.add_config_value("httpexample_scheme", "http", "html")
|
|
38
|
+
app.add_lexer("http-response", HTTPResponseLexer)
|
|
39
|
+
dist = metadata.distribution("sphinxcontrib-httpexample")
|
|
40
|
+
return {"version": dist.version}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from ast import unparse
|
|
3
|
+
from io import StringIO
|
|
4
|
+
from shlex import quote as shlex_quote
|
|
5
|
+
from sphinxcontrib.httpexample.utils import is_json
|
|
6
|
+
from sphinxcontrib.httpexample.utils import maybe_str
|
|
7
|
+
from urllib.parse import parse_qs
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
import ast
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import string
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_find_unsafe = re.compile(
|
|
16
|
+
r"[^\w@%+=:,./-" + string.ascii_letters + string.digits + "]",
|
|
17
|
+
).search
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def shlex_double_quote(s):
|
|
21
|
+
"""Return a shell-escaped version of the string *s*."""
|
|
22
|
+
if not s:
|
|
23
|
+
return '""'
|
|
24
|
+
if _find_unsafe(s) is None:
|
|
25
|
+
return s
|
|
26
|
+
|
|
27
|
+
# use double quotes, and put double quotes into single quotes
|
|
28
|
+
# the string $"b is then quoted as "$"'"'"b"
|
|
29
|
+
return re.sub(r'^""|""$', "", ('"' + s.replace('"', '"\'"\'"') + '"'))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
EXCLUDE_HEADERS = [
|
|
33
|
+
"Authorization",
|
|
34
|
+
"Host",
|
|
35
|
+
]
|
|
36
|
+
EXCLUDE_HEADERS_HTTP = EXCLUDE_HEADERS + []
|
|
37
|
+
EXCLUDE_HEADERS_REQUESTS = EXCLUDE_HEADERS + []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_curl_command(request):
|
|
41
|
+
parts = ["curl", "-i"]
|
|
42
|
+
|
|
43
|
+
# Method
|
|
44
|
+
parts.append("-X {}".format(request.command))
|
|
45
|
+
|
|
46
|
+
# URL
|
|
47
|
+
parts.append(shlex_quote(request.url()))
|
|
48
|
+
|
|
49
|
+
# Authorization (prepare)
|
|
50
|
+
method, token = request.auth()
|
|
51
|
+
|
|
52
|
+
# Headers
|
|
53
|
+
for header in sorted(request.headers):
|
|
54
|
+
if header in EXCLUDE_HEADERS:
|
|
55
|
+
continue
|
|
56
|
+
header_line = shlex_double_quote(
|
|
57
|
+
"{}: {}".format(header, request.headers[header]),
|
|
58
|
+
)
|
|
59
|
+
parts.append("-H {}".format(header_line))
|
|
60
|
+
|
|
61
|
+
if method != "Basic" and "Authorization" in request.headers:
|
|
62
|
+
header = "Authorization"
|
|
63
|
+
header_line = shlex_double_quote(
|
|
64
|
+
"{}: {}".format(header, request.headers[header]),
|
|
65
|
+
)
|
|
66
|
+
parts.append("-H {}".format(header_line))
|
|
67
|
+
|
|
68
|
+
# JSON
|
|
69
|
+
data = maybe_str(request.data())
|
|
70
|
+
if data:
|
|
71
|
+
if is_json(request.headers.get("Content-Type", "")):
|
|
72
|
+
data = json.dumps(data)
|
|
73
|
+
parts.append("--data-raw '{}'".format(data))
|
|
74
|
+
|
|
75
|
+
# Authorization
|
|
76
|
+
if method == "Basic":
|
|
77
|
+
parts.append("--user {}".format(token))
|
|
78
|
+
|
|
79
|
+
return " ".join(parts)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_wget_command(request):
|
|
83
|
+
parts = ["wget", "-S", "-O-"]
|
|
84
|
+
|
|
85
|
+
# Method
|
|
86
|
+
if request.command not in ["GET", "POST"]:
|
|
87
|
+
parts.append("--method={}".format(request.command))
|
|
88
|
+
|
|
89
|
+
# URL
|
|
90
|
+
parts.append(shlex_quote(request.url()))
|
|
91
|
+
|
|
92
|
+
# Authorization (prepare)
|
|
93
|
+
method, token = request.auth()
|
|
94
|
+
|
|
95
|
+
# Headers
|
|
96
|
+
for header in sorted(request.headers):
|
|
97
|
+
if header in EXCLUDE_HEADERS:
|
|
98
|
+
continue
|
|
99
|
+
header_line = shlex_double_quote(
|
|
100
|
+
"{}: {}".format(header, request.headers[header]),
|
|
101
|
+
)
|
|
102
|
+
parts.append("--header={}".format(header_line))
|
|
103
|
+
|
|
104
|
+
if method != "Basic" and "Authorization" in request.headers:
|
|
105
|
+
header = "Authorization"
|
|
106
|
+
header_line = shlex_double_quote(
|
|
107
|
+
"{}: {}".format(header, request.headers[header])
|
|
108
|
+
)
|
|
109
|
+
parts.append("--header={}".format(header_line))
|
|
110
|
+
|
|
111
|
+
# JSON or raw data
|
|
112
|
+
data = maybe_str(request.data())
|
|
113
|
+
if data:
|
|
114
|
+
if is_json(request.headers.get("Content-Type", "")):
|
|
115
|
+
data = json.dumps(data)
|
|
116
|
+
if request.command == "POST":
|
|
117
|
+
parts.append("--post-data='{}'".format(data))
|
|
118
|
+
elif request.command != "POST":
|
|
119
|
+
parts.append("--body-data='{}'".format(data))
|
|
120
|
+
|
|
121
|
+
# Authorization
|
|
122
|
+
if method == "Basic":
|
|
123
|
+
user, password = token.split(":")
|
|
124
|
+
parts.append("--auth-no-challenge")
|
|
125
|
+
parts.append("--user={}".format(user))
|
|
126
|
+
parts.append("--password={}".format(password))
|
|
127
|
+
|
|
128
|
+
return " ".join(parts)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_httpie_command(request):
|
|
132
|
+
parts = ["http"]
|
|
133
|
+
redir_input = ""
|
|
134
|
+
|
|
135
|
+
# Method
|
|
136
|
+
if request.command != "GET":
|
|
137
|
+
parts.append(request.command)
|
|
138
|
+
|
|
139
|
+
# URL
|
|
140
|
+
parts.append(shlex_quote(request.url()))
|
|
141
|
+
|
|
142
|
+
# Authorization (prepare)
|
|
143
|
+
method, token = request.auth()
|
|
144
|
+
|
|
145
|
+
# Headers
|
|
146
|
+
for header in sorted(request.headers):
|
|
147
|
+
if header in EXCLUDE_HEADERS_HTTP:
|
|
148
|
+
continue
|
|
149
|
+
parts.append(
|
|
150
|
+
"{}:{}".format(
|
|
151
|
+
header,
|
|
152
|
+
shlex_double_quote(request.headers[header]),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if method != "Basic" and "Authorization" in request.headers:
|
|
157
|
+
header = "Authorization"
|
|
158
|
+
parts.append(
|
|
159
|
+
"{}:{}".format(
|
|
160
|
+
header,
|
|
161
|
+
shlex_double_quote(request.headers[header]),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# JSON or raw data
|
|
166
|
+
data = maybe_str(request.data())
|
|
167
|
+
if data:
|
|
168
|
+
|
|
169
|
+
if is_json(request.headers.get("Content-Type", "")):
|
|
170
|
+
# We need to explicitly set the separators to get consistent
|
|
171
|
+
# whitespace handling across Python 2 and 3. See
|
|
172
|
+
# https://bugs.python.org/issue16333 for details.
|
|
173
|
+
redir_input = shlex_quote(
|
|
174
|
+
json.dumps(data, indent=2, sort_keys=True, separators=(",", ": "))
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
redir_input = shlex_quote(data)
|
|
178
|
+
|
|
179
|
+
# Authorization
|
|
180
|
+
if method == "Basic":
|
|
181
|
+
parts.append("-a {}".format(token))
|
|
182
|
+
|
|
183
|
+
cmd = " ".join(parts)
|
|
184
|
+
|
|
185
|
+
if not redir_input:
|
|
186
|
+
return cmd
|
|
187
|
+
|
|
188
|
+
else:
|
|
189
|
+
return "echo {} | {}".format(redir_input, cmd)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def flatten_parsed_qs(data):
|
|
193
|
+
"""Flatten single value lists in parse_qs results."""
|
|
194
|
+
for key, value in data.items():
|
|
195
|
+
if isinstance(value, list) and len(value) == 1:
|
|
196
|
+
data[key] = value[0]
|
|
197
|
+
return data
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_requests_command(request):
|
|
201
|
+
# Method
|
|
202
|
+
tree = ast.parse("requests.{}()".format(request.command.lower()))
|
|
203
|
+
call = tree.body[0].value
|
|
204
|
+
call.keywords = []
|
|
205
|
+
|
|
206
|
+
# URL
|
|
207
|
+
call.args.append(ast.Constant(request.url()))
|
|
208
|
+
|
|
209
|
+
# Authorization (prepare)
|
|
210
|
+
method, token = request.auth()
|
|
211
|
+
|
|
212
|
+
# Headers
|
|
213
|
+
header_keys = []
|
|
214
|
+
header_values = []
|
|
215
|
+
for header in sorted(request.headers):
|
|
216
|
+
if header in EXCLUDE_HEADERS_REQUESTS:
|
|
217
|
+
continue
|
|
218
|
+
header_keys.append(ast.Constant(header))
|
|
219
|
+
header_values.append(ast.Constant(request.headers[header]))
|
|
220
|
+
if method != "Basic" and "Authorization" in request.headers:
|
|
221
|
+
header_keys.append(ast.Constant("Authorization"))
|
|
222
|
+
header_values.append(ast.Constant(request.headers["Authorization"]))
|
|
223
|
+
if header_keys and header_values:
|
|
224
|
+
call.keywords.append(
|
|
225
|
+
ast.keyword("headers", ast.Dict(header_keys, header_values))
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# JSON or raw data
|
|
229
|
+
data = maybe_str(request.data())
|
|
230
|
+
|
|
231
|
+
# Form data
|
|
232
|
+
content_type = request.headers.get("Content-Type")
|
|
233
|
+
if content_type == "application/x-www-form-urlencoded":
|
|
234
|
+
if not isinstance(data, dict):
|
|
235
|
+
data = flatten_parsed_qs(parse_qs(data))
|
|
236
|
+
|
|
237
|
+
def astify_json_obj(obj):
|
|
238
|
+
obj = maybe_str(obj)
|
|
239
|
+
if isinstance(obj, str):
|
|
240
|
+
return ast.Constant(obj)
|
|
241
|
+
elif isinstance(obj, bool):
|
|
242
|
+
return ast.Name(str(obj), ast.Load())
|
|
243
|
+
elif isinstance(obj, int):
|
|
244
|
+
return ast.Name(str(obj), ast.Load())
|
|
245
|
+
elif isinstance(obj, float):
|
|
246
|
+
return ast.Name(str(obj), ast.Load())
|
|
247
|
+
elif isinstance(obj, list):
|
|
248
|
+
json_values = []
|
|
249
|
+
for v in obj:
|
|
250
|
+
json_values.append(astify_json_obj(v))
|
|
251
|
+
return ast.List(json_values, ast.Load())
|
|
252
|
+
elif isinstance(obj, dict):
|
|
253
|
+
json_values = []
|
|
254
|
+
json_keys = []
|
|
255
|
+
for k, v in obj.items():
|
|
256
|
+
json_keys.append(ast.Constant(maybe_str(k)))
|
|
257
|
+
json_values.append(astify_json_obj(v))
|
|
258
|
+
return ast.Dict(json_keys, json_values)
|
|
259
|
+
else:
|
|
260
|
+
raise Exception("Cannot astify {0:s}".format(str(obj)))
|
|
261
|
+
|
|
262
|
+
if data:
|
|
263
|
+
if is_json(request.headers.get("Content-Type", "")):
|
|
264
|
+
call.keywords.append(ast.keyword("json", astify_json_obj(data)))
|
|
265
|
+
else:
|
|
266
|
+
call.keywords.append(ast.keyword("data", ast.Constant(data)))
|
|
267
|
+
|
|
268
|
+
# Authorization
|
|
269
|
+
if method == "Basic":
|
|
270
|
+
token = maybe_str(token)
|
|
271
|
+
call.keywords.append(
|
|
272
|
+
ast.keyword(
|
|
273
|
+
"auth", ast.Tuple(tuple(map(ast.Constant, token.split(":"))), None)
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return unparse(tree).strip()
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from docutils import nodes
|
|
3
|
+
from docutils.parsers.rst import directives
|
|
4
|
+
from docutils.statemachine import StringList
|
|
5
|
+
from sphinx.directives.code import CodeBlock
|
|
6
|
+
from sphinxcontrib.httpexample import builders
|
|
7
|
+
from sphinxcontrib.httpexample import parsers
|
|
8
|
+
from sphinxcontrib.httpexample import utils
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
AVAILABLE_BUILDERS = {
|
|
14
|
+
"curl": (builders.build_curl_command, "shell"),
|
|
15
|
+
"wget": (builders.build_wget_command, "shell"),
|
|
16
|
+
"httpie": (builders.build_httpie_command, "shell"),
|
|
17
|
+
"python-requests": (builders.build_requests_command, "python"),
|
|
18
|
+
"requests": (builders.build_requests_command, "python"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
AVAILABLE_FIELDS = ["query"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def choose_builders(arguments):
|
|
25
|
+
return [
|
|
26
|
+
directives.choice(argument, AVAILABLE_BUILDERS)
|
|
27
|
+
for argument in (arguments or [])
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_builder(name, builder, language, label):
|
|
32
|
+
AVAILABLE_BUILDERS[name] = (builder, language, label)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HTTPExample(CodeBlock):
|
|
36
|
+
|
|
37
|
+
required_arguments = 0
|
|
38
|
+
optional_arguments = len(AVAILABLE_BUILDERS)
|
|
39
|
+
|
|
40
|
+
option_spec = utils.merge_dicts(
|
|
41
|
+
CodeBlock.option_spec,
|
|
42
|
+
{
|
|
43
|
+
"request": directives.unchanged,
|
|
44
|
+
"response": directives.unchanged,
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def process_content(content):
|
|
50
|
+
if content:
|
|
51
|
+
raw = ("\r\n".join(content)).encode("utf-8")
|
|
52
|
+
request = parsers.parse_request(raw)
|
|
53
|
+
params, _ = request.extract_fields("query")
|
|
54
|
+
params = [(p[1], p[2]) for p in params]
|
|
55
|
+
new_path = utils.add_url_params(request.path, params)
|
|
56
|
+
content[0] = " ".join([request.command, new_path, request.request_version])
|
|
57
|
+
|
|
58
|
+
# split the request and optional response in the content.
|
|
59
|
+
# The separator is two empty lines followed by a line starting with
|
|
60
|
+
# 'HTTP/' or 'HTTP '
|
|
61
|
+
request_content = StringList()
|
|
62
|
+
request_content_no_fields = StringList()
|
|
63
|
+
response_content = None
|
|
64
|
+
emptylines_count = 0
|
|
65
|
+
in_response = False
|
|
66
|
+
is_field = r":({}) (.+): (.+)".format("|".join(AVAILABLE_FIELDS))
|
|
67
|
+
for i, line in enumerate(content):
|
|
68
|
+
source = content.source(i)
|
|
69
|
+
if in_response:
|
|
70
|
+
response_content.append(line, source)
|
|
71
|
+
else:
|
|
72
|
+
if emptylines_count >= 2 and (
|
|
73
|
+
line.startswith("HTTP/") or line.startswith("HTTP ")
|
|
74
|
+
):
|
|
75
|
+
in_response = True
|
|
76
|
+
response_content = StringList()
|
|
77
|
+
response_content.append(line, source)
|
|
78
|
+
elif line == "":
|
|
79
|
+
emptylines_count += 1
|
|
80
|
+
else:
|
|
81
|
+
request_content.extend(StringList([""] * emptylines_count, source))
|
|
82
|
+
request_content.append(line, source)
|
|
83
|
+
|
|
84
|
+
if not re.match(is_field, line):
|
|
85
|
+
request_content_no_fields.extend(
|
|
86
|
+
StringList([""] * emptylines_count, source)
|
|
87
|
+
)
|
|
88
|
+
request_content_no_fields.append(line, source)
|
|
89
|
+
|
|
90
|
+
emptylines_count = 0
|
|
91
|
+
|
|
92
|
+
return (request_content, request_content_no_fields, response_content)
|
|
93
|
+
|
|
94
|
+
def run(self):
|
|
95
|
+
if self.content:
|
|
96
|
+
processed = self.process_content(StringList(self.content))
|
|
97
|
+
have_request = bool(processed[1])
|
|
98
|
+
have_response = bool(processed[2])
|
|
99
|
+
else:
|
|
100
|
+
have_request = "request" in self.options
|
|
101
|
+
have_response = "response" in self.options
|
|
102
|
+
|
|
103
|
+
# Wrap and render main directive as 'http-example-http'
|
|
104
|
+
klass = "http-example-http"
|
|
105
|
+
container = nodes.container("", classes=[klass])
|
|
106
|
+
container.append(nodes.caption("", "http"))
|
|
107
|
+
block = HTTPExampleBlock(
|
|
108
|
+
"http:example-block",
|
|
109
|
+
["http"],
|
|
110
|
+
self.options,
|
|
111
|
+
self.content,
|
|
112
|
+
self.lineno,
|
|
113
|
+
self.content_offset,
|
|
114
|
+
self.block_text,
|
|
115
|
+
self.state,
|
|
116
|
+
self.state_machine,
|
|
117
|
+
)
|
|
118
|
+
container.extend(block.run())
|
|
119
|
+
|
|
120
|
+
# Init result node list
|
|
121
|
+
result = [container]
|
|
122
|
+
|
|
123
|
+
# Append builder responses
|
|
124
|
+
if have_request:
|
|
125
|
+
for argument in self.arguments:
|
|
126
|
+
builder_entry = AVAILABLE_BUILDERS.get(argument)
|
|
127
|
+
if builder_entry is not None and len(builder_entry) == 3:
|
|
128
|
+
label = builder_entry[2]
|
|
129
|
+
else:
|
|
130
|
+
label = argument
|
|
131
|
+
options = self.options.copy()
|
|
132
|
+
options.pop("name", None)
|
|
133
|
+
options.pop("caption", None)
|
|
134
|
+
|
|
135
|
+
block = HTTPExampleBlock(
|
|
136
|
+
"http:example-block",
|
|
137
|
+
[argument],
|
|
138
|
+
options,
|
|
139
|
+
self.content,
|
|
140
|
+
self.lineno,
|
|
141
|
+
self.content_offset,
|
|
142
|
+
self.block_text,
|
|
143
|
+
self.state,
|
|
144
|
+
self.state_machine,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Wrap and render main directive as 'http-example-{name}'
|
|
148
|
+
klass = "http-example-{}".format(argument)
|
|
149
|
+
container = nodes.container("", classes=[klass])
|
|
150
|
+
container.append(nodes.caption("", label))
|
|
151
|
+
container.extend(block.run())
|
|
152
|
+
|
|
153
|
+
# Append to result nodes
|
|
154
|
+
result.append(container)
|
|
155
|
+
|
|
156
|
+
# Append optional response
|
|
157
|
+
if have_response:
|
|
158
|
+
options = self.options.copy()
|
|
159
|
+
options.pop("name", None)
|
|
160
|
+
options.pop("caption", None)
|
|
161
|
+
|
|
162
|
+
block = HTTPExampleBlock(
|
|
163
|
+
"http:example-block",
|
|
164
|
+
["response"],
|
|
165
|
+
options,
|
|
166
|
+
self.content,
|
|
167
|
+
self.lineno,
|
|
168
|
+
self.content_offset,
|
|
169
|
+
self.block_text,
|
|
170
|
+
self.state,
|
|
171
|
+
self.state_machine,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Wrap and render main directive as 'http-example-response'
|
|
175
|
+
klass = "http-example-response"
|
|
176
|
+
container = nodes.container("", classes=[klass])
|
|
177
|
+
container.append(nodes.caption("", "response"))
|
|
178
|
+
container.extend(block.run())
|
|
179
|
+
|
|
180
|
+
# Append to result nodes
|
|
181
|
+
result.append(container)
|
|
182
|
+
|
|
183
|
+
# Final wrap
|
|
184
|
+
container_node = nodes.container("", classes=["http-example"])
|
|
185
|
+
container_node.extend(result)
|
|
186
|
+
|
|
187
|
+
return [container_node]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class HTTPExampleBlock(CodeBlock):
|
|
191
|
+
required_arguments = 1
|
|
192
|
+
|
|
193
|
+
option_spec = utils.merge_dicts(
|
|
194
|
+
CodeBlock.option_spec,
|
|
195
|
+
{
|
|
196
|
+
"request": directives.unchanged,
|
|
197
|
+
"response": directives.unchanged,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def read_http_file(self, path):
|
|
202
|
+
cwd = os.path.dirname(self.state.document.current_source)
|
|
203
|
+
request = utils.resolve_path(path, cwd)
|
|
204
|
+
with open(request) as fp:
|
|
205
|
+
return StringList(list(map(str.rstrip, fp.readlines())), request)
|
|
206
|
+
|
|
207
|
+
def run(self):
|
|
208
|
+
if self.arguments == ["http"]:
|
|
209
|
+
if "request" in self.options:
|
|
210
|
+
self.content = self.read_http_file(self.options["request"])
|
|
211
|
+
else:
|
|
212
|
+
self.content = HTTPExample.process_content(self.content)[1]
|
|
213
|
+
elif self.arguments == ["response"]:
|
|
214
|
+
if "response" in self.options:
|
|
215
|
+
self.content = self.read_http_file(self.options["response"])
|
|
216
|
+
else:
|
|
217
|
+
self.content = HTTPExample.process_content(self.content)[2]
|
|
218
|
+
self.arguments = ["http-response"]
|
|
219
|
+
else:
|
|
220
|
+
if "request" in self.options:
|
|
221
|
+
request_content_no_fields = self.read_http_file(self.options["request"])
|
|
222
|
+
else:
|
|
223
|
+
request_content_no_fields = HTTPExample.process_content(self.content)[1]
|
|
224
|
+
|
|
225
|
+
raw = ("\r\n".join(request_content_no_fields)).encode("utf-8")
|
|
226
|
+
|
|
227
|
+
config = self.env.config
|
|
228
|
+
request = parsers.parse_request(raw, config.httpexample_scheme)
|
|
229
|
+
name = choose_builders(self.arguments)[0]
|
|
230
|
+
try:
|
|
231
|
+
builder_, language, _ = AVAILABLE_BUILDERS[name]
|
|
232
|
+
except ValueError:
|
|
233
|
+
builder_, language = AVAILABLE_BUILDERS[name]
|
|
234
|
+
self.arguments = [language]
|
|
235
|
+
|
|
236
|
+
command = builder_(request)
|
|
237
|
+
self.content = StringList([command], request_content_no_fields.source(0))
|
|
238
|
+
|
|
239
|
+
return super(HTTPExampleBlock, self).run()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""A custom Pygments lexer for HTTP responses."""
|
|
3
|
+
|
|
4
|
+
from pygments.lexer import bygroups
|
|
5
|
+
from pygments.lexer import Lexer
|
|
6
|
+
from pygments.lexer import RegexLexer
|
|
7
|
+
from pygments.lexers import get_lexer_for_mimetype
|
|
8
|
+
from pygments.lexers import TextLexer
|
|
9
|
+
from pygments.token import Name
|
|
10
|
+
from pygments.token import String
|
|
11
|
+
from pygments.token import Text
|
|
12
|
+
from pygments.token import Token
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTTPHeaderLexer(RegexLexer):
|
|
17
|
+
"""A simple lexer for HTTP headers."""
|
|
18
|
+
|
|
19
|
+
name = "HTTPHeader"
|
|
20
|
+
tokens = {
|
|
21
|
+
"root": [
|
|
22
|
+
(
|
|
23
|
+
r"^([a-zA-Z][a-zA-Z0-9-]*)(:\s*)(.*)",
|
|
24
|
+
bygroups(Name.Attribute, Text, String),
|
|
25
|
+
),
|
|
26
|
+
(r".+", Text),
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HTTPResponseLexer(Lexer):
|
|
32
|
+
"""
|
|
33
|
+
A custom Pygments lexer that delegates the body parsing to another lexer
|
|
34
|
+
based on the Content-Type header.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name = "HTTPResponse"
|
|
38
|
+
aliases = ["http-response"]
|
|
39
|
+
|
|
40
|
+
def get_tokens_unprocessed(self, text, **options):
|
|
41
|
+
# 1. Split headers and body
|
|
42
|
+
parts = re.split(r"(\r?\n\r?\n)", text, maxsplit=1)
|
|
43
|
+
header_part = parts[0]
|
|
44
|
+
|
|
45
|
+
# 2. Handle the Status Line to avoid Token.Error
|
|
46
|
+
# Supports: "HTTP 200 OK", "HTTP/1.1 200 OK", "HTTP/2 200"
|
|
47
|
+
lines = header_part.splitlines(keepends=True)
|
|
48
|
+
if lines:
|
|
49
|
+
status_line = lines[0]
|
|
50
|
+
if status_line.startswith("HTTP"):
|
|
51
|
+
yield 0, Token.Keyword.Reserved, status_line.rstrip()
|
|
52
|
+
if status_line.endswith("\n") or status_line.endswith("\r\n"):
|
|
53
|
+
yield len(status_line.rstrip()), Token.Text, status_line[
|
|
54
|
+
len(status_line.rstrip()) :
|
|
55
|
+
]
|
|
56
|
+
header_remainder = "".join(lines[1:])
|
|
57
|
+
else:
|
|
58
|
+
header_remainder = header_part
|
|
59
|
+
else:
|
|
60
|
+
header_remainder = ""
|
|
61
|
+
|
|
62
|
+
# 3. Parse remaining headers
|
|
63
|
+
h_lexer = HTTPHeaderLexer()
|
|
64
|
+
current_offset = len(lines[0]) if lines and lines[0].startswith("HTTP") else 0
|
|
65
|
+
for line in header_remainder.splitlines(keepends=True):
|
|
66
|
+
for pos, token, value in h_lexer.get_tokens_unprocessed(line):
|
|
67
|
+
yield current_offset + pos, token, value
|
|
68
|
+
current_offset += len(line)
|
|
69
|
+
|
|
70
|
+
if len(parts) < 3:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# 4. Yield the separator
|
|
74
|
+
separator = parts[1]
|
|
75
|
+
body_part = parts[2]
|
|
76
|
+
yield len(header_part), Token.Text, separator
|
|
77
|
+
|
|
78
|
+
# 5. Delegate body parsing
|
|
79
|
+
ct_match = re.search(
|
|
80
|
+
r"^Content-Type:\s*([^;\n\r\s]+)", header_part, re.I | re.M
|
|
81
|
+
)
|
|
82
|
+
mime = ct_match.group(1).strip() if ct_match else "text/plain"
|
|
83
|
+
try:
|
|
84
|
+
body_lexer = get_lexer_for_mimetype(mime)
|
|
85
|
+
except Exception:
|
|
86
|
+
body_lexer = TextLexer()
|
|
87
|
+
|
|
88
|
+
body_offset = len(header_part) + len(separator)
|
|
89
|
+
for pos, token, value in body_lexer.get_tokens_unprocessed(
|
|
90
|
+
body_part, **options
|
|
91
|
+
):
|
|
92
|
+
yield body_offset + pos, token, value
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from sphinxcontrib.httpexample.utils import add_url_params
|
|
4
|
+
from sphinxcontrib.httpexample.utils import capitalize_keys
|
|
5
|
+
from sphinxcontrib.httpexample.utils import is_json
|
|
6
|
+
from sphinxcontrib.httpexample.utils import ordered
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from http.server import BaseHTTPRequestHandler
|
|
14
|
+
except ImportError:
|
|
15
|
+
from BaseHTTPServer import BaseHTTPRequestHandler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
AVAILABLE_FIELDS = ["query"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPRequest(BaseHTTPRequestHandler):
|
|
22
|
+
# http://stackoverflow.com/a/5955949
|
|
23
|
+
|
|
24
|
+
scheme = "http"
|
|
25
|
+
|
|
26
|
+
# noinspection PyMissingConstructor
|
|
27
|
+
def __init__(self, request_bytes, scheme):
|
|
28
|
+
assert isinstance(request_bytes, bytes)
|
|
29
|
+
|
|
30
|
+
self.scheme = scheme
|
|
31
|
+
self.rfile = BytesIO(request_bytes)
|
|
32
|
+
self.raw_requestline = self.rfile.readline()
|
|
33
|
+
self.error_code = self.error_message = None
|
|
34
|
+
self.parse_request()
|
|
35
|
+
|
|
36
|
+
if self.error_message:
|
|
37
|
+
raise Exception(self.error_message)
|
|
38
|
+
|
|
39
|
+
# Replace headers with simple dict to coup differences in Py2 and Py3
|
|
40
|
+
self.headers = capitalize_keys(dict(getattr(self, "headers", {})))
|
|
41
|
+
|
|
42
|
+
def send_error(self, code, message=None, explain=None):
|
|
43
|
+
self.error_code = code
|
|
44
|
+
self.error_message = message
|
|
45
|
+
|
|
46
|
+
def extract_fields(self, field=None, available_fields=None):
|
|
47
|
+
if available_fields is None:
|
|
48
|
+
available_fields = AVAILABLE_FIELDS
|
|
49
|
+
|
|
50
|
+
if (field is not None) and field not in available_fields:
|
|
51
|
+
msg = "Unexpected field '{}'. Expected one of {}."
|
|
52
|
+
msg = msg.format(field, ", ".join(available_fields))
|
|
53
|
+
raise ValueError(msg)
|
|
54
|
+
|
|
55
|
+
if field is None:
|
|
56
|
+
field = "|".join(available_fields)
|
|
57
|
+
is_field = r":({}) (.+): (.+)".format(field)
|
|
58
|
+
|
|
59
|
+
fields = []
|
|
60
|
+
remaining_request = []
|
|
61
|
+
cursor = self.rfile.tell()
|
|
62
|
+
for i, line in enumerate(self.rfile.readlines()):
|
|
63
|
+
line = line.decode("utf-8")
|
|
64
|
+
try:
|
|
65
|
+
field, key, val = re.match(is_field, line).groups()
|
|
66
|
+
except AttributeError:
|
|
67
|
+
remaining_request.append(line)
|
|
68
|
+
continue
|
|
69
|
+
fields.append((field.strip(), key.strip(), val.strip()))
|
|
70
|
+
|
|
71
|
+
remaining_request = BytesIO(
|
|
72
|
+
"\n".join(remaining_request).encode("utf-8").strip()
|
|
73
|
+
)
|
|
74
|
+
remaining_request.seek(0)
|
|
75
|
+
self.rfile.seek(cursor)
|
|
76
|
+
|
|
77
|
+
return (fields, remaining_request)
|
|
78
|
+
|
|
79
|
+
def auth(self):
|
|
80
|
+
try:
|
|
81
|
+
method, token = self.headers.get("Authorization").split()
|
|
82
|
+
except (AttributeError, KeyError, ValueError):
|
|
83
|
+
return None, None
|
|
84
|
+
if not isinstance(token, bytes):
|
|
85
|
+
token = token.encode("utf-8")
|
|
86
|
+
if method == "Basic":
|
|
87
|
+
return method, base64.b64decode(token).decode("utf-8")
|
|
88
|
+
else:
|
|
89
|
+
return method, token
|
|
90
|
+
|
|
91
|
+
def url(self):
|
|
92
|
+
base_url = "{}://{}{}".format(
|
|
93
|
+
self.scheme, self.headers.get("Host", "nohost"), self.path
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
params, _ = self.extract_fields("query")
|
|
97
|
+
params = [(p[1], p[2]) for p in params]
|
|
98
|
+
|
|
99
|
+
if params:
|
|
100
|
+
new_url = add_url_params(base_url, params)
|
|
101
|
+
else:
|
|
102
|
+
new_url = base_url
|
|
103
|
+
|
|
104
|
+
return new_url
|
|
105
|
+
|
|
106
|
+
def data(self):
|
|
107
|
+
_, payload_bytes = self.extract_fields(None)
|
|
108
|
+
payload_bytes = payload_bytes.read()
|
|
109
|
+
if payload_bytes:
|
|
110
|
+
if is_json(self.headers.get("Content-Type", "")):
|
|
111
|
+
assert isinstance(payload_bytes, bytes)
|
|
112
|
+
payload_str = payload_bytes.decode("utf-8")
|
|
113
|
+
return ordered(json.loads(payload_str))
|
|
114
|
+
else:
|
|
115
|
+
return payload_bytes
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_request(request_bytes, scheme="http"):
|
|
119
|
+
return HTTPRequest(request_bytes, scheme)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.http-example .caption,
|
|
2
|
+
.section ul .http-example .caption {
|
|
3
|
+
display: inline-block;
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
padding: 0.5em 1em;
|
|
6
|
+
margin: 0;
|
|
7
|
+
}
|
|
8
|
+
.http-example .caption.selected {
|
|
9
|
+
font-weight: bold;
|
|
10
|
+
background-color: transparent;
|
|
11
|
+
border: 1px solid #e1e4e5;
|
|
12
|
+
border-bottom: 1px solid white;
|
|
13
|
+
}
|
|
14
|
+
.http-example div[class^='highlight'] {
|
|
15
|
+
margin-top: -1px;
|
|
16
|
+
}
|
|
17
|
+
.http-example div[class^='highlight'] pre {
|
|
18
|
+
white-space: break-spaces;
|
|
19
|
+
}
|
|
20
|
+
.http-example div.highlight-bash pre {
|
|
21
|
+
white-space: pre-wrap;
|
|
22
|
+
}
|
|
23
|
+
.http-example-http div[class^='highlight'] pre,
|
|
24
|
+
.http-example-httpie div[class^='highlight'] pre,
|
|
25
|
+
.http-example-plone-client div[class^='highlight'] pre,
|
|
26
|
+
.http-example-python-requests div[class^='highlight'] pre,
|
|
27
|
+
.http-example-wget div[class^='highlight'] pre,
|
|
28
|
+
.http-example-response div[class^='highlight'] pre {
|
|
29
|
+
white-space: pre;
|
|
30
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
2
|
+
// Select all elements with the class 'http-example container'
|
|
3
|
+
var containers = document.querySelectorAll('.http-example.container');
|
|
4
|
+
containers.forEach(function(container) {
|
|
5
|
+
|
|
6
|
+
// Get all child elements
|
|
7
|
+
var blocks = Array.from(container.children);
|
|
8
|
+
|
|
9
|
+
// Find caption elements or derive them from elements with the 'caption-text' class
|
|
10
|
+
var captions = container.querySelectorAll('.caption');
|
|
11
|
+
if (captions.length === 0) {
|
|
12
|
+
captions = container.querySelectorAll('.caption-text');
|
|
13
|
+
captions.forEach(function(caption) {
|
|
14
|
+
caption.classList.add('caption');
|
|
15
|
+
});
|
|
16
|
+
} else {
|
|
17
|
+
captions = Array.from(captions);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Process each caption element
|
|
21
|
+
captions.forEach(function(caption, index) {
|
|
22
|
+
var orphan, block = !!caption.parentElement.id ? caption.parentElement : caption.parentElement.parentElement;
|
|
23
|
+
block.setAttribute('role', 'tabpanel');
|
|
24
|
+
block.setAttribute('tabindex', '0');
|
|
25
|
+
block.setAttribute('aria-labelledby', block.id + '-label');
|
|
26
|
+
|
|
27
|
+
caption.id = block.id + '-label';
|
|
28
|
+
caption.setAttribute('role', 'tab');
|
|
29
|
+
caption.setAttribute('tabindex', '0');
|
|
30
|
+
caption.setAttribute('aria-label', caption.textContent.trim());
|
|
31
|
+
caption.setAttribute('aria-controls', block.id);
|
|
32
|
+
|
|
33
|
+
// Click event for captions
|
|
34
|
+
caption.addEventListener('click', function() {
|
|
35
|
+
captions.forEach(function(otherCaption) {
|
|
36
|
+
if (caption === otherCaption) {
|
|
37
|
+
// Select the clicked caption
|
|
38
|
+
caption.setAttribute('aria-selected', 'true');
|
|
39
|
+
caption.classList.add('selected');
|
|
40
|
+
otherCaption.setAttribute('tabindex', '0');
|
|
41
|
+
} else {
|
|
42
|
+
// Deselect other captions
|
|
43
|
+
otherCaption.setAttribute('aria-selected', 'false');
|
|
44
|
+
otherCaption.setAttribute('tabindex', '-1');
|
|
45
|
+
otherCaption.classList.remove('selected');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Hide other blocks and show the selected one
|
|
50
|
+
blocks.forEach(function(otherBlock) {
|
|
51
|
+
if (otherBlock !== block) {
|
|
52
|
+
otherBlock.style.display = 'none';
|
|
53
|
+
otherBlock.setAttribute('hidden', 'hidden');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
block.style.display = '';
|
|
57
|
+
block.removeAttribute('hidden');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Keydown event for accessibility
|
|
61
|
+
caption.addEventListener('keydown', function(event) {
|
|
62
|
+
if (event.code === 'Space' || event.code === 'Enter') {
|
|
63
|
+
caption.click();
|
|
64
|
+
} else if (event.code === 'ArrowRight') {
|
|
65
|
+
// Move focus to the next tab
|
|
66
|
+
var nextIndex = (index + 1) % captions.length;
|
|
67
|
+
captions[nextIndex].focus();
|
|
68
|
+
captions[nextIndex].click();
|
|
69
|
+
} else if (event.code === 'ArrowLeft') {
|
|
70
|
+
// Move focus to the previous tab
|
|
71
|
+
var prevIndex = (index - 1 + captions.length) % captions.length;
|
|
72
|
+
captions[prevIndex].focus();
|
|
73
|
+
captions[prevIndex].click();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (caption.classList.contains("caption-text")) {
|
|
78
|
+
orphan = caption.parentElement;
|
|
79
|
+
container.appendChild(caption);
|
|
80
|
+
orphan.remove();
|
|
81
|
+
} else {
|
|
82
|
+
container.appendChild(caption);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Set ARIA role for the container
|
|
87
|
+
container.setAttribute('role', 'tablist');
|
|
88
|
+
|
|
89
|
+
// Append blocks back to the container
|
|
90
|
+
blocks.forEach(function(block) {
|
|
91
|
+
container.appendChild(block);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Automatically click the first caption to set the initial state
|
|
95
|
+
if (captions.length > 0) {
|
|
96
|
+
captions[0].click();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from importlib import resources
|
|
4
|
+
from urllib.parse import parse_qsl
|
|
5
|
+
from urllib.parse import ParseResult
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_dicts(a, b):
|
|
13
|
+
c = a.copy()
|
|
14
|
+
c.update(b)
|
|
15
|
+
return c
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_path(spec, cwd=""):
|
|
19
|
+
if os.path.isfile(os.path.normpath(os.path.join(cwd, spec))):
|
|
20
|
+
return os.path.normpath(os.path.join(cwd, spec))
|
|
21
|
+
elif spec.count(":"):
|
|
22
|
+
package, resource = spec.split(":", 1)
|
|
23
|
+
resource_path = resources.files(package) / resource
|
|
24
|
+
if resource_path.exists():
|
|
25
|
+
return str(resource_path)
|
|
26
|
+
else:
|
|
27
|
+
return spec
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def maybe_str(v):
|
|
31
|
+
"""Convert any strings to local 'str' instances"""
|
|
32
|
+
if isinstance(v, str) and isinstance(v, bytes):
|
|
33
|
+
return v # Python 2 encoded
|
|
34
|
+
elif str(type(v)) == "<type 'unicode'>":
|
|
35
|
+
return v.encode("utf-8") # Python 2 unicode
|
|
36
|
+
elif isinstance(v, bytes):
|
|
37
|
+
return v.decode("utf-8") # Python 3 encoded
|
|
38
|
+
elif isinstance(v, str):
|
|
39
|
+
return v # Python 3 unicode
|
|
40
|
+
else:
|
|
41
|
+
return v # not a string
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ordered(dict_):
|
|
45
|
+
if isinstance(dict_, dict):
|
|
46
|
+
# http://stackoverflow.com/a/22721724
|
|
47
|
+
results = OrderedDict()
|
|
48
|
+
for k, v in sorted(dict_.items()):
|
|
49
|
+
results[k] = ordered(v)
|
|
50
|
+
else:
|
|
51
|
+
results = dict_
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def capitalize(s):
|
|
56
|
+
return "-".join(map(str.capitalize, s.split("-")))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def capitalize_keys(d):
|
|
60
|
+
return dict([(capitalize(k), v) for k, v in d.items()])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_json(content_type):
|
|
64
|
+
"""Checks if the given content type should be treated as JSON.
|
|
65
|
+
|
|
66
|
+
The primary use cases to be recognized as JSON are
|
|
67
|
+
|
|
68
|
+
- `application/json` mimetype
|
|
69
|
+
- `+json` structured syntax suffix
|
|
70
|
+
"""
|
|
71
|
+
parts = {part.strip() for part in content_type.lower().strip().split(";")}
|
|
72
|
+
if "application/json" in parts:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
for p in parts:
|
|
76
|
+
if p.endswith("+json"):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def add_url_params(url, params):
|
|
83
|
+
"""Add GET query parameters to provided URL.
|
|
84
|
+
|
|
85
|
+
https://stackoverflow.com/a/25580545/1262843
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
url (str): target URL
|
|
89
|
+
params (list of tuples): query parameters to be added
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
new_url (str): updated URL
|
|
93
|
+
"""
|
|
94
|
+
url = unquote(url)
|
|
95
|
+
parsed_url = urlparse(url)
|
|
96
|
+
new_params = parse_qsl(parsed_url.query) + params
|
|
97
|
+
new_params_encoded = urlencode(new_params, doseq=True)
|
|
98
|
+
new_url = ParseResult(
|
|
99
|
+
parsed_url.scheme,
|
|
100
|
+
parsed_url.netloc,
|
|
101
|
+
parsed_url.path,
|
|
102
|
+
parsed_url.params,
|
|
103
|
+
new_params_encoded,
|
|
104
|
+
parsed_url.fragment,
|
|
105
|
+
).geturl()
|
|
106
|
+
|
|
107
|
+
return new_url
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinxcontrib-httpexample
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Summary: Adds example directive for sphinx-contrib httpdomain
|
|
5
|
+
Project-URL: Repository, https://github.com/collective/sphinxcontrib-httpexample
|
|
6
|
+
Author-email: Asko Soukka <asko.soukka@iki.fi>
|
|
7
|
+
License: GPL-2.0-only
|
|
8
|
+
Keywords: documentation,extension,http,rest,sphinx
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Documentation
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: docutils
|
|
21
|
+
Requires-Dist: importlib-metadata
|
|
22
|
+
Requires-Dist: importlib-resources
|
|
23
|
+
Requires-Dist: sphinx
|
|
24
|
+
Requires-Dist: sphinxcontrib-httpdomain
|
|
25
|
+
Provides-Extra: sphinx747
|
|
26
|
+
Requires-Dist: sphinx==7.4.7; (python_version >= '3.10') and extra == 'sphinx747'
|
|
27
|
+
Provides-Extra: sphinx802
|
|
28
|
+
Requires-Dist: sphinx==8.0.2; (python_version >= '3.10') and extra == 'sphinx802'
|
|
29
|
+
Provides-Extra: sphinx813
|
|
30
|
+
Requires-Dist: babel; extra == 'sphinx813'
|
|
31
|
+
Requires-Dist: sphinx==8.1.3; (python_version >= '3.10') and extra == 'sphinx813'
|
|
32
|
+
Provides-Extra: sphinx823
|
|
33
|
+
Requires-Dist: sphinx==8.2.3; (python_version >= '3.11') and extra == 'sphinx823'
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# sphinxcontrib-httpexample
|
|
38
|
+
|
|
39
|
+
<img alt="GitHub Actions" src="https://github.com/collective/sphinxcontrib-httpexample/actions/workflows/build.yml/badge.svg?branch=master" href="https://github.com/collective/sphinxcontrib-httpexample/actions">
|
|
40
|
+
<img alt="Coverage" src="https://coveralls.io/repos/github/collective/sphinxcontrib-httpexample/badge.svg?branch=master" href="https://coveralls.io/github/collective/sphinxcontrib-httpexample?branch=master">
|
|
41
|
+
<img alt="PyPI package" src="https://badge.fury.io/py/sphinxcontrib-httpexample.svg" href="https://pypi.org/project/sphinxcontrib-httpexample/">
|
|
42
|
+
<img alt="Documentation" src="https://readthedocs.org/projects/sphinxcontrib-httpexample/badge/?version=latest" href="https://sphinxcontrib-httpexample.readthedocs.io/en/latest/">
|
|
43
|
+
|
|
44
|
+
sphinxcontrib-httpexample is a Sphinx domain extension for describing RESTful HTTP APIs in detail.
|
|
45
|
+
It enhances [`sphinxcontrib-httpdomain`](https://github.com/sphinx-contrib/httpdomain) with a simple call example directive.
|
|
46
|
+
The directive provided by this extension generates RESTful HTTP API call examples for different HTTP clients from a single HTTP request example.
|
|
47
|
+
|
|
48
|
+
The audience for this extension are developers and technical writers documenting their RESTful HTTP APIs.
|
|
49
|
+
This extension was originally developed for documenting [`plone.restapi`](https://6.docs.plone.org/plone.restapi/docs/source/index.html).
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- Directive for generating various RESTful HTTP API call examples from a single HTTP request.
|
|
55
|
+
- Supported HTTP clients:
|
|
56
|
+
|
|
57
|
+
- [curl](https://curl.haxx.se/)
|
|
58
|
+
- [wget](https://www.gnu.org/software/wget/)
|
|
59
|
+
- [httpie](https://httpie.io/)
|
|
60
|
+
- [requests](https://requests.readthedocs.io/en/stable/)
|
|
61
|
+
|
|
62
|
+
- Custom builders, such as the [`@plone/client`](https://www.npmjs.com/package/@plone/client) package, an agnostic library that provides easy access to the Plone REST API from a client written in TypeScript.
|
|
63
|
+
See https://sphinxcontrib-httpexample.readthedocs.io/en/latest/custom.html for examples.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
This extension has been used in documentation for the following projects and probably other similar projects as well.
|
|
69
|
+
|
|
70
|
+
- https://6.docs.plone.org/plone.restapi/docs/source/index.html
|
|
71
|
+
- https://sphinxcontrib-httpexample.readthedocs.io/en/latest/
|
|
72
|
+
- https://guillotina.readthedocs.io/en/latest/
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Full documentation for end users can be found in the `docs` folder.
|
|
78
|
+
It's also available online at https://sphinxcontrib-httpexample.readthedocs.io/en/latest/.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Add `sphinxcontrib-httpexample` and `sphincontrib-httpdomain` to your project requirements.
|
|
84
|
+
|
|
85
|
+
Then configure your Sphinx configuration file `conf.py` with `sphinxcontrib.httpdomain` and `sphinxcontrib.httpexample` as follows.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
extensions = [
|
|
89
|
+
"sphinxcontrib.httpdomain",
|
|
90
|
+
"sphinxcontrib.httpexample",
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Contribute
|
|
96
|
+
|
|
97
|
+
To contribute to `sphinxcontrib-httpexample`, first set up your environment.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
### Set up development environment
|
|
101
|
+
|
|
102
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/).
|
|
103
|
+
Carefully read the console output for further instruction.
|
|
104
|
+
|
|
105
|
+
```shell
|
|
106
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Initialize a Python virtual environment.
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
uv venv
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Install `sphinxcontrib-httpexample`.
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
uv sync
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
### Build documentation
|
|
123
|
+
|
|
124
|
+
Rebuild Sphinx documentation on changes, with live reload in the browser.
|
|
125
|
+
|
|
126
|
+
```shell
|
|
127
|
+
make livehtml
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To stop the preview, type `CTRL-C`.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### Run tests
|
|
134
|
+
|
|
135
|
+
```shell
|
|
136
|
+
make test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
The project is licensed under the GPLv2.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sphinxcontrib/httpexample/__init__.py,sha256=h7PxhqRDKIZkMrenlot9TKnhRc4jUd0sqGGm5fhx74g,1404
|
|
2
|
+
sphinxcontrib/httpexample/builders.py,sha256=JJD9n4tA9qVBkLA20EZVbF08R3uu0Q1feWB8DWXFhcw,8088
|
|
3
|
+
sphinxcontrib/httpexample/directives.py,sha256=Uy5rcnafU2j2Ynk-HHbS078HwzV7MhLyRtyWVQT7RrM,8434
|
|
4
|
+
sphinxcontrib/httpexample/lexers.py,sha256=-OYFJiluVjB5JCunghvLr8yCexsJxwrxLrQLBa64DfM,3045
|
|
5
|
+
sphinxcontrib/httpexample/parsers.py,sha256=mJkfAebpnpVvVsKxmd6Qs4Uk8jTrS2gBYLmWn-kgIUs,3788
|
|
6
|
+
sphinxcontrib/httpexample/utils.py,sha256=sk4jMhr2OJ1ZAf35t7jFZ4kYdUh9B5VTyFXuSZ-6Vt0,2771
|
|
7
|
+
sphinxcontrib/httpexample/static/sphinxcontrib-httpexample.css,sha256=_gughUpi98ryQU_Nq3IKDInDLdb20fL_xVvRmDXhaZE,858
|
|
8
|
+
sphinxcontrib/httpexample/static/sphinxcontrib-httpexample.js,sha256=7pz_9jiaS_LKsiDKrb5nsJbdBa4lf3VALyifUhpgzw0,3594
|
|
9
|
+
sphinxcontrib_httpexample-2.0.dist-info/METADATA,sha256=V58sp7gjTcAz8yf_fjg2Ex0F6jizgyfWdbwukAdWd24,5071
|
|
10
|
+
sphinxcontrib_httpexample-2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
sphinxcontrib_httpexample-2.0.dist-info/RECORD,,
|