Skip to content

Commit 17ddeb4

Browse files
committed
Basic bootstrap CSS
1 parent bd93a3e commit 17ddeb4

12 files changed

+132
-6
lines changed

pypi_view/app.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import email
12
import io
23
import mimetypes
34
import os.path
5+
import re
46

57
import pygments.lexers
68
import pygments.lexers.special
79
import fluffy_code.code
810
import fluffy_code.prebuilt_styles
911
from identify import identify
12+
from starlette.middleware.base import BaseHTTPMiddleware
13+
from starlette.middleware import Middleware
1014
from starlette.applications import Starlette
1115
from starlette.staticfiles import StaticFiles
1216
from starlette.requests import Request
1317
from starlette.responses import Response
1418
from starlette.responses import PlainTextResponse
19+
from starlette.responses import RedirectResponse
1520
from starlette.responses import StreamingResponse
1621
from starlette.routing import Route
1722
from starlette.templating import Jinja2Templates
@@ -55,7 +60,22 @@
5560

5661
install_root = os.path.dirname(__file__)
5762

58-
app = Starlette(debug=os.environ.get("PYPI_VIEW_DEBUG") == "1")
63+
64+
class CacheControlHeaderMiddleware(BaseHTTPMiddleware):
65+
66+
async def dispatch(self, request, call_next):
67+
response = await call_next(request)
68+
# TODO: There should be a better way to do this...
69+
response.headers["Cache-Control"] = "no-cache"
70+
return response
71+
72+
73+
app = Starlette(
74+
debug=os.environ.get("PYPI_VIEW_DEBUG") == "1",
75+
middleware=[
76+
Middleware(CacheControlHeaderMiddleware),
77+
],
78+
)
5979
app.mount('/static', StaticFiles(directory=os.path.join(install_root, 'static')), name='static')
6080

6181
templates = Jinja2Templates(
@@ -115,13 +135,37 @@ async def package_file(request: Request) -> Response:
115135
)
116136

117137
entries = await package.entries()
138+
metadata_entries = [
139+
entry
140+
for entry in entries
141+
if re.match(r'(?:[^/]+\.dist-info/METADATA|^[^/]+/PKG-INFO)$', entry.path) and entry.size <= TEXT_RENDER_FILESIZE_LIMIT
142+
]
143+
if len(metadata_entries) > 0:
144+
metadata_path = metadata_entries[0].path
145+
metadata = {}
146+
147+
async with package.open_from_archive(metadata_path) as f:
148+
metadata_file = io.StringIO((await f.read()).decode("utf8", errors="ignore"))
149+
150+
message = email.message_from_file(metadata_file)
151+
metadata = {
152+
key: message.get_all(key)
153+
for key in set(message.keys())
154+
}
155+
else:
156+
metadata_path = None
157+
metadata = {}
158+
159+
118160
return templates.TemplateResponse(
119161
"package_file.html",
120162
{
121163
"request": request,
122164
"package": package_name,
123165
"filename": file_name,
124166
"entries": entries,
167+
"metadata_path": metadata_path,
168+
"metadata": metadata,
125169
},
126170
)
127171

@@ -172,7 +216,7 @@ async def transfer_file():
172216

173217
return StreamingResponse(
174218
transfer_file(),
175-
media_type=mimetype if mimetype.startswith(MIME_WHITELIST) else None,
219+
media_type=mimetype if (mimetype or "").startswith(MIME_WHITELIST) else None,
176220
headers={"Content-Length": str(entry.size)},
177221
)
178222

@@ -256,3 +300,8 @@ async def transfer_file():
256300
"error": "This file appears to be a binary.",
257301
},
258302
)
303+
304+
305+
@app.route('/search')
306+
async def search(request: Request) -> Response:
307+
return RedirectResponse(request.url_for('package', package=request.query_params['package']))

pypi_view/packaging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async def __aenter__(self) -> "AsyncArchiveFile":
5757
async def __aexit__(self, exc_t, exc_v, exc_tb) -> None:
5858
await asyncio.to_thread(self.file_.close)
5959

60-
async def read(self, n_bytes: typing.Optional[int]) -> bytes:
60+
async def read(self, n_bytes: typing.Optional[int] = None) -> bytes:
6161
return await asyncio.to_thread(self.file_.read, n_bytes)
6262

6363

pypi_view/static/bootstrap-5.2.1.min.css

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pypi_view/static/bootstrap-5.2.1.min.js

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pypi_view/static/site.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
body {
2-
background-color: pink;
32
}

pypi_view/static/site.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
document.getElementById('package-search').onfocus = (e) => e.target.select();

pypi_view/templates/base.html

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
<!doctype html>
2-
<html>
2+
<html lang="en">
33
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<link rel="stylesheet" href="{{url_for('static', path='/bootstrap-5.2.1.min.css')}}" />
47
<link rel="stylesheet" href="{{url_for('static', path='/site.css')}}" />
8+
<title>{% block title %}{% endblock %}</title>
59
{% block extra_head %}{% endblock %}
610
</head>
711
<body class="page-{{page}}">
8-
{% block content %}{% endblock %}
12+
<nav class="navbar navbar-dark bg-dark">
13+
<div class="container">
14+
<a class="navbar-brand" href="{{url_for('home')}}">PyPi View</a>
15+
<form class="d-flex" method="GET" action="{{url_for('search')}}">
16+
<input
17+
id="package-search"
18+
class="form-control me-2"
19+
type="text"
20+
name="package"
21+
placeholder="Package name"
22+
value="{{selected_package_name}}"
23+
/>
24+
<button class="btn btn-outline-success" type="submit">Go</button>
25+
</form>
26+
<div class="collapse navbar-collapse">
27+
foo
28+
</div>
29+
</div>
30+
</nav>
31+
32+
<div class="container">
33+
{% block content %}{% endblock %}
34+
</div>
35+
936
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
37+
<script src="{{url_for('static', path='/bootstrap-5.2.1.min.js')}}"></script>
38+
<script src="{{url_for('static', path='/site.js')}}"></script>
1039
{% block extra_js %}{% endblock %}
1140
</body>
1241
</html>

pypi_view/templates/home.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{% set page = 'home' %}
22
{% extends 'base.html' %}
33

4+
{% block title %}PyPi View{% endblock %}
5+
46
{# vim: ft=jinja
57
#}

pypi_view/templates/package.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{% set page = 'package' %}
22
{% extends 'base.html' %}
33

4+
{% block title %}{{package}} | PyPi View{% endblock %}
5+
46
{% block content %}
57
<h1>{{package}}</h1>
68
{% for version in version_to_files|sort %}

pypi_view/templates/package_file.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
{% set page = 'package-file' %}
2+
{% set selected_package_name = package %}
23
{% extends 'base.html' %}
34

5+
{% block title %}{{filename}} | {{package}} | PyPi View{% endblock %}
6+
47
{% block content %}
58
<h1>{{filename}}</h1>
9+
10+
{% if metadata_path %}
11+
<h2>Metadata</h2>
12+
<table border="1">
13+
{% for key, values in metadata.items()|sort %}
14+
{% for value in values %}
15+
<tr>
16+
{% if loop.index == 1 %}
17+
<th rowspan="{{values|length}}">{{key}}</th>
18+
{% endif %}
19+
<td>{{value}}</td>
20+
</tr>
21+
{% endfor %}
22+
{% endfor %}
23+
</table>
24+
<a href="{{url_for('package_file_archive_path', package=package, filename=filename, archive_path=metadata_path)}}">
25+
View Raw Metadata
26+
</a>
27+
{% endif %}
28+
29+
<h2>Files</h2>
630
<ul>
731
{% for entry in entries|sort(attribute="path") %}
832
<li>

0 commit comments

Comments
 (0)