Skip to content

Commit 9fc0167

Browse files
committed
first commit
1 parent 1c9e139 commit 9fc0167

File tree

8 files changed

+367
-0
lines changed

8 files changed

+367
-0
lines changed

.github/workflows/build.yml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: build
2+
3+
on: [push]
4+
5+
jobs:
6+
lint:
7+
name: Lint
8+
runs-on: ubuntu-latest
9+
timeout-minutes: 5
10+
steps:
11+
- uses: actions/checkout@v3
12+
- name: Set up Python ${{ matrix.python-version }}
13+
uses: actions/setup-python@v4
14+
with:
15+
python-version: "3.10"
16+
- name: Lint code
17+
run: |
18+
make setup
19+
make check
20+
21+
deploy:
22+
name: Deploy on PyPI
23+
needs: lint
24+
runs-on: ubuntu-latest
25+
environment: release
26+
permissions:
27+
id-token: write
28+
timeout-minutes: 10
29+
steps:
30+
- uses: actions/checkout@v3
31+
- name: Set up Python ${{ matrix.python-version }}
32+
uses: actions/setup-python@v4
33+
with:
34+
python-version: 3.8
35+
- name: Install pypa/build
36+
run: |
37+
python -m pip install --upgrade pip
38+
python -m pip install --upgrade setuptools wheel
39+
python -m pip install build --user
40+
- name: Build a binary wheel and a source tarball
41+
run: |
42+
python -m build --sdist --wheel --outdir dist/
43+
- name: Publish package distributions to PyPI
44+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
45+
uses: pypa/gh-action-pypi-publish@release/v1

Makefile

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.PHONY: setup format lint check clean ci
2+
3+
setup:
4+
pip install -e .
5+
pip install -r requirements-dev.txt
6+
7+
format:
8+
black notebook_httpdbg
9+
10+
lint:
11+
black --check notebook_httpdbg
12+
flake8 notebook_httpdbg
13+
14+
check: lint
15+
16+
clean:
17+
rm -rf .pytest_cache
18+
rm -rf __pycache__
19+
rm -rf notebook_httpdbg.egg-info
20+
rm -rf venv
21+
rm -rf build
22+
rm -rf dist
23+
24+
ci:
25+
python -m pip install pip --upgrade
26+
python -m pip install setuptools wheel --upgrade
27+
pip install -r requirements-dev.txt
28+
pip install .

example.ipynb

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "ccb1095c-c112-4858-a2b4-5bd885423958",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"%load_ext notebook_httpdbg"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"id": "b6119d2a-b03b-41d4-881a-b1afbc87a1b9",
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"import requests"
21+
]
22+
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": null,
26+
"id": "f9b3f680-4e2e-477b-a46d-83e726314576",
27+
"metadata": {
28+
"scrolled": true
29+
},
30+
"outputs": [],
31+
"source": [
32+
"%%httpdbg\n",
33+
"_ = requests.get(\"https://www.example.com\")"
34+
]
35+
},
36+
{
37+
"cell_type": "code",
38+
"execution_count": null,
39+
"id": "ae9f2db9-eff0-4c5b-8f4f-1147a2b60c0e",
40+
"metadata": {},
41+
"outputs": [],
42+
"source": []
43+
}
44+
],
45+
"metadata": {
46+
"kernelspec": {
47+
"display_name": "Python 3 (ipykernel)",
48+
"language": "python",
49+
"name": "python3"
50+
},
51+
"language_info": {
52+
"codemirror_mode": {
53+
"name": "ipython",
54+
"version": 3
55+
},
56+
"file_extension": ".py",
57+
"mimetype": "text/x-python",
58+
"name": "python",
59+
"nbconvert_exporter": "python",
60+
"pygments_lexer": "ipython3",
61+
"version": "3.10.12"
62+
}
63+
},
64+
"nbformat": 4,
65+
"nbformat_minor": 5
66+
}

notebook_httpdbg/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from notebook_httpdbg.main import load_ipython_extension
2+
from notebook_httpdbg.main import unload_ipython_extension
3+
4+
__all__ = ["load_ipython_extension", "unload_ipython_extension"]
5+
6+
VERSION = "0.0.1"

notebook_httpdbg/main.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import argparse
2+
import base64
3+
import json
4+
import html
5+
import time
6+
from typing import Tuple, Dict, List
7+
import urllib
8+
9+
from IPython.core.interactiveshell import InteractiveShell
10+
from IPython import get_ipython
11+
from IPython.display import display, HTML
12+
13+
import notebook
14+
15+
from httpdbg import httprecord
16+
17+
18+
def css() -> str:
19+
style = """<style>
20+
.httpdbg-block {
21+
border-style: solid !important;
22+
border-width: 1px 0px !important;
23+
border-color: rgb(124, 124, 124) !important;
24+
width: 100% !important;
25+
}
26+
27+
.httpdbg-url {
28+
padding: 0px 0px 0px 10px !important;
29+
}
30+
31+
.httpdbg-url-data {
32+
padding: 0px 0px 0px 10px !important;
33+
}
34+
35+
.httpdbg-url-code {
36+
padding: 0px 0px 0px 10px !important;
37+
width: 100% !important;
38+
background-color: #EEEEEE !important;
39+
cursor: default !important;
40+
}
41+
42+
.httpdbg-url-code > img {
43+
max-width: 50% !important;
44+
height: auto !important;
45+
}
46+
47+
.httpdbg-expand {
48+
cursor: pointer !important;
49+
}
50+
"""
51+
52+
if int(notebook.__version__.split(".")[0]) < 7:
53+
style += """
54+
55+
.httpdbg-expand > summary:before {
56+
content: '\\229E ' !important;
57+
}
58+
59+
.httpdbg-expand[open] > summary:before {
60+
content: '\\229F ' !important;
61+
}
62+
"""
63+
64+
style += """
65+
</style>
66+
"""
67+
68+
return style
69+
70+
71+
def parse_content_type(content_type: str) -> Tuple[str, Dict[str, str]]:
72+
s = content_type.split(";")
73+
media_type = s[0]
74+
directives = {}
75+
for directive in s[1:]:
76+
sp = directive.split("=")
77+
if len(sp) == 2:
78+
directives[sp[0].strip()] = sp[1].strip()
79+
return media_type, directives
80+
81+
82+
def print_body(req, limit: int) -> str:
83+
if len(req.content) == 0:
84+
return ""
85+
86+
preview = req.preview
87+
88+
if preview.get("image"):
89+
return f"""<img src="data:{preview["content_type"]};base64,{base64.b64encode(req.content).decode("utf-8")}">"""
90+
91+
if preview.get("text"):
92+
txt = preview["text"]
93+
try:
94+
txt = json.dumps(json.loads(preview["text"]), indent=4)
95+
except Exception:
96+
# query string
97+
try:
98+
if (
99+
parse_content_type(req.get_header("content_type"))[0]
100+
== "application/x-www-form-urlencoded"
101+
):
102+
qs = []
103+
for key, value in urllib.parse.parse_qsl(
104+
req.content, strict_parsing=True
105+
):
106+
if value:
107+
qs.append(f"{key}={value}")
108+
if qs:
109+
txt = "\n\n".join(qs)
110+
except Exception:
111+
pass
112+
113+
return html.escape(txt)[:limit]
114+
115+
return "<i> body data not printable </i>"
116+
117+
118+
def print_headers(headers: List[str], limit: int):
119+
return html.escape(
120+
"\n".join([f"{header.name}: {header.value}" for header in headers])
121+
)[:limit]
122+
123+
124+
def httpdbg_magic(line, cell):
125+
args = read_args([arg for arg in line.strip().split(" ") if arg != ""])
126+
127+
with httprecord() as records:
128+
tbegin = time.time()
129+
get_ipython().run_cell(cell)
130+
tend = time.time()
131+
infos = css()
132+
infos += f"""<details class="httpdbg-block httpdbg-expand"><summary>[httpdbg] {len(records)} requests in {tend-tbegin:.2f} seconds</summary>"""
133+
for record in records:
134+
request_headers = print_headers(record.request.headers, args.headers)
135+
request_body = print_body(
136+
record.request,
137+
args.body,
138+
)
139+
response_headers = print_headers(record.response.headers, args.headers)
140+
response_body = print_body(
141+
record.response,
142+
args.body,
143+
)
144+
infos += f"""<details class="httpdbg-expand httpdbg-url">
145+
<summary>{html.escape(str(record.status_code))} {html.escape(record.method)} {html.escape(record.url)}</summary>
146+
<details class="httpdbg-expand httpdbg-url-data"><summary>request</summary><pre class="httpdbg-url-code">{request_headers}\n\n{str(request_body)}</pre></details>
147+
<details class="httpdbg-expand httpdbg-url-data"><summary>response</summary><pre class="httpdbg-url-code">{response_headers}\n\n{str(response_body)}</pre></details>
148+
</details>"""
149+
display(HTML(infos))
150+
151+
152+
def read_args(args: List[str]) -> argparse.Namespace:
153+
parser = argparse.ArgumentParser(
154+
prog="httpdbg",
155+
description="httdbg - a very simple tool to debug HTTP(S) client requests",
156+
)
157+
158+
parser.add_argument(
159+
"--headers",
160+
type=int,
161+
default=500,
162+
metavar=500,
163+
help="Number of characters to display for the headers.",
164+
)
165+
166+
parser.add_argument(
167+
"--body",
168+
type=int,
169+
default=500,
170+
metavar=500,
171+
help="Number of characters to display for the body.",
172+
)
173+
174+
return parser.parse_args(args)
175+
176+
177+
def load_ipython_extension(ipython: InteractiveShell):
178+
ipython.register_magic_function(httpdbg_magic, "cell", "httpdbg")
179+
180+
181+
def unload_ipython_extension(ipython):
182+
pass

pyproject.toml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "notebook-httpdbg"
7+
authors = [
8+
{name = "cle-b", email = "[email protected]"},
9+
]
10+
description="A notebook extension to trace HTTP(S) requests."
11+
readme="README.md"
12+
urls = {repository = "https://github.com/cle-b/notebook-httpdbg"}
13+
requires-python = ">=3.7.0"
14+
license = {text = "Apache-2.0"}
15+
classifiers = [
16+
"Development Status :: 3 - Alpha",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: Apache Software License",
19+
"Operating System :: OS Independent",
20+
"Programming Language :: Python :: 3",
21+
"Framework :: Jupyter"
22+
]
23+
dynamic = ["version"]
24+
dependencies = [
25+
"httpdbg >= 0.17.0",
26+
"notebook"
27+
]
28+
29+
[tool.setuptools]
30+
packages = ["notebook_httpdbg"]
31+
32+
[tool.setuptools.dynamic]
33+
version = {attr = "notebook_httpdbg.VERSION"}
34+

requirements-dev.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
flake8
2+
black

setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
exclude = .git,venv,.tox
3+
max-line-length = 88
4+
extend-ignore = E203, E501

0 commit comments

Comments
 (0)