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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any