Skip to content

Commit 5d9c95c

Browse files
committed
Update from 45b7b33
1 parent cd75201 commit 5d9c95c

File tree

11 files changed

+215
-319
lines changed

11 files changed

+215
-319
lines changed

javascript/scss/app.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ $container-max-widths: (
1919
color: white;
2020
}
2121

22-
.nav-link:hover {
22+
.nav-link:hover, .nav-link:focus {
2323
color: white;
2424
}
2525

@@ -145,3 +145,8 @@ img {
145145
background: $base-color2 !important;
146146
color: white !important;
147147
}
148+
149+
150+
.m-1em {
151+
margin: 1em;
152+
}

pyproject.toml

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,68 @@
66
# or submit itself to any jurisdiction.
77

88
[build-system]
9-
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"]
9+
requires = ["setuptools>=61", "setuptools_scm>=8"]
10+
build-backend = "setuptools.build_meta"
11+
12+
[project]
13+
name = "simple-repository-browser"
14+
dynamic = ["version"]
15+
description = "A web interface to browse and search packages in any simple package repository (PEP-503), inspired by PyPI / warehouse"
16+
requires-python = ">=3.11"
17+
classifiers = [
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: OS Independent",
20+
"Programming Language :: Python :: 3",
21+
]
22+
authors = [
23+
{name = "Phil Elson"},
24+
{name = "Francesco Iannaccone"},
25+
{name = "Ivan Sinkarenko"},
26+
]
27+
dependencies = [
28+
"httpx",
29+
"aiosqlite",
30+
"diskcache",
31+
"docutils",
32+
"fastapi",
33+
"importlib_metadata>=6.0",
34+
"jinja2",
35+
"markdown",
36+
"markupsafe",
37+
"packaging",
38+
"parsley",
39+
"pkginfo",
40+
"readme-renderer[md]",
41+
"simple-repository>=0.6",
42+
"uvicorn",
43+
]
44+
readme = "README.md"
45+
46+
[project.urls]
47+
Homepage = "https://github.com/simple-repository/simple-repository-browser"
48+
49+
[project.optional-dependencies]
50+
test = [
51+
"pytest",
52+
]
53+
dev = [
54+
"simple-repository-browser[test]",
55+
]
56+
57+
[project.scripts]
58+
simple-repository-browser = "simple_repository_browser.__main__:main"
59+
1060

1161
[tool.setuptools_scm]
12-
write_to = "simple_repository_browser/_version.py"
62+
version_file = "simple_repository_browser/_version.py"
1363

1464
[[tool.mypy.overrides]]
1565
module = [
1666
"diskcache",
1767
"parsley",
1868
]
1969
ignore_missing_imports = true
70+
71+
[tool.setuptools.packages.find]
72+
include = ["simple_repository_browser", "simple_repository_browser.*"]
73+
namespaces = false

setup.py

Lines changed: 0 additions & 90 deletions
This file was deleted.

simple_repository_browser/__main__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
2626
# parsed arguments.
2727
parser.set_defaults(handler=handler)
2828

29-
parser.add_argument("index_url", type=str, nargs='?', default='https://pypi.org/simple/')
29+
parser.add_argument("repository_url", type=str, nargs='?', default='https://pypi.org/simple/')
3030
parser.add_argument("--host", default="0.0.0.0")
3131
parser.add_argument("--port", type=int, default=8080)
3232
parser.add_argument("--cache-dir", type=str, default=Path(os.environ.get('XDG_CACHE_DIR', Path.home() / '.cache')) / 'simple-repository-browser')
@@ -37,7 +37,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
3737

3838
def handler(args: typing.Any) -> None:
3939
app = AppBuilder(
40-
index_url=args.index_url,
40+
repository_url=args.repository_url,
4141
cache_dir=Path(args.cache_dir),
4242
template_paths=[
4343
args.templates_dir,
@@ -69,5 +69,7 @@ def main():
6969

7070

7171
if __name__ == '__main__':
72-
logging.basicConfig(level=logging.WARNING)
72+
logging.basicConfig(level=logging.INFO)
73+
logging.getLogger('httpcore').setLevel(logging.WARNING)
74+
logging.getLogger('httpx').setLevel(logging.WARNING)
7375
main()

simple_repository_browser/_app.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import sqlite3
1010
import typing
1111
from pathlib import Path
12+
from urllib.parse import urlparse
1213

1314
import aiosqlite
1415
import diskcache
1516
import fastapi
17+
import fastapi.responses
1618
import httpx
1719
from simple_repository import SimpleRepository
1820
from simple_repository.components.http import HttpRepository
21+
from simple_repository.components.local import LocalRepository
1922

2023
from . import controller, crawler, errors, fetch_projects, model, view
2124
from .metadata_injector import MetadataInjector
@@ -25,15 +28,15 @@ class AppBuilder:
2528
def __init__(
2629
self,
2730
url_prefix: str,
28-
index_url: str,
31+
repository_url: str,
2932
cache_dir: Path,
3033
template_paths: typing.Sequence[Path],
3134
static_files_path: Path,
3235
crawl_popular_projects: bool,
3336
browser_version: str,
3437
) -> None:
3538
self.url_prefix = url_prefix
36-
self.index_url = index_url
39+
self.repository_url = repository_url
3740
self.cache_dir = cache_dir
3841
self.template_paths = template_paths
3942
self.static_files_path = static_files_path
@@ -55,7 +58,7 @@ def create_app(self) -> fastapi.FastAPI:
5558

5659
async def lifespan(app: fastapi.FastAPI):
5760
async with (
58-
httpx.AsyncClient() as http_client,
61+
httpx.AsyncClient(timeout=30) as http_client,
5962
aiosqlite.connect(self.db_path, timeout=5) as db,
6063
):
6164
_controller = self.create_controller(
@@ -67,6 +70,15 @@ async def lifespan(app: fastapi.FastAPI):
6770
)
6871
router = _controller.create_router(self.static_files_path)
6972
app.mount(self.url_prefix or "/", router)
73+
74+
if self.url_prefix:
75+
# If somebody visits the root URL, and that isn't index (because we are
76+
# using a prefix) just redirect them to the index page. This is super
77+
# convenient for development purposes.
78+
@app.get("/")
79+
async def redirect_to_index():
80+
return fastapi.responses.RedirectResponse(url=app.url_path_for('index'))
81+
7082
yield
7183

7284
app = fastapi.FastAPI(
@@ -84,7 +96,7 @@ async def catch_exceptions_middleware(request: fastapi.Request, call_next):
8496
status_code = 500
8597
detail = f"Internal server error ({err})"
8698
# raise
87-
logging.error(err)
99+
logging.exception(err)
88100
content = _view.error_page(
89101
request=request,
90102
context=model.ErrorModel(detail=detail),
@@ -110,13 +122,18 @@ def create_crawler(self, http_client: httpx.AsyncClient, source: SimpleRepositor
110122
cache=self.cache,
111123
)
112124

125+
def _repo_from_url(self, url: str, http_client: httpx.AsyncClient) -> SimpleRepository:
126+
if urlparse(url).scheme in ("http", "https"):
127+
return HttpRepository(
128+
url=url,
129+
http_client=http_client,
130+
)
131+
else:
132+
return LocalRepository(Path(url))
133+
113134
def create_model(self, http_client: httpx.AsyncClient, database: aiosqlite.Connection) -> model.Model:
114135
source = MetadataInjector(
115-
HttpRepository(
116-
url=self.index_url,
117-
http_client=http_client,
118-
),
119-
database=database,
136+
self._repo_from_url(self.repository_url, http_client=http_client),
120137
http_client=http_client,
121138
)
122139
return model.Model(

simple_repository_browser/crawler.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import diskcache
1616
import httpx
17-
from packaging.requirements import InvalidRequirement
17+
from packaging.requirements import InvalidRequirement, Requirement
1818
from packaging.utils import canonicalize_name
1919
from packaging.version import Version
2020
from simple_repository import SimpleRepository, model
@@ -55,19 +55,22 @@ async def crawl_recursively(
5555
normalized_project_names_to_crawl: typing.Set[str],
5656
) -> None:
5757
"""
58-
Crawl the matadata of the packages in
59-
normalized_project_names_to_crawl and
58+
Crawl the metadata of the packages in normalized_project_names_to_crawl and
6059
of their dependencies.
6160
"""
6261
seen: set = set()
6362
packages_for_reindexing = set(normalized_project_names_to_crawl)
6463
while packages_for_reindexing - seen:
6564
remaining_packages = packages_for_reindexing - seen
6665
pkg_name = remaining_packages.pop()
67-
logging.info(
66+
logging.debug(
6867
f"Index iteration loop. Looking at {pkg_name}, with {len(remaining_packages)} remaining ({len(seen)} having been completed)",
6968
)
7069
seen.add(pkg_name)
70+
if len(seen) % 100 == 0:
71+
logging.info(
72+
f"Index iteration batch of 100 complete. {len(seen)} completed, {len(remaining_packages)} remaining",
73+
)
7174
try:
7275
prj = await self._source.get_project_page(pkg_name)
7376
except PackageNotFoundError:
@@ -97,8 +100,9 @@ async def crawl_recursively(
97100
continue
98101

99102
for dist in pkg_info.requires_dist:
100-
dep_name = dist.name
101-
packages_for_reindexing.add(canonicalize_name(dep_name))
103+
if isinstance(dist, Requirement):
104+
dep_name = dist.name
105+
packages_for_reindexing.add(canonicalize_name(dep_name))
102106

103107
# Don't DOS the service, we aren't in a rush here.
104108
await asyncio.sleep(0.01)
@@ -109,7 +113,7 @@ async def refetch_hook(self) -> None:
109113
# We periodically want to refresh the project database to make sure we are up-to-date.
110114
await fetch_projects.fully_populate_db(
111115
connection=self._projects_db,
112-
index=self._source,
116+
repository=self._source,
113117
)
114118
packages_w_dist_info = set()
115119
for cache_type, name, version in self._cache:
@@ -118,7 +122,7 @@ async def refetch_hook(self) -> None:
118122

119123
popular_projects = []
120124
if self._crawl_popular_projects:
121-
# Add the top 100 packages (and their dependencies) to the index
125+
# Add the top 100 packages (and their dependencies) to the repository
122126
URL = 'https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json'
123127
try:
124128
resp = await self._http_client.get(URL)
@@ -129,10 +133,12 @@ async def refetch_hook(self) -> None:
129133
logging.warning(f'Problem fetching popular projects ({err})')
130134
pass
131135

132-
await self.crawl_recursively(packages_w_dist_info | set(popular_projects))
136+
projects_to_crawl = packages_w_dist_info | set(popular_projects)
137+
logging.info(f'About to start crawling {len(projects_to_crawl)} projects (and their transient dependencies)')
138+
await self.crawl_recursively(projects_to_crawl)
133139

134140
async def run_reindex_periodically(self) -> None:
135-
logging.info("Starting the reindexing loop")
141+
logging.debug("Starting the reindexing loop")
136142
while True:
137143
try:
138144
await self.refetch_hook()

0 commit comments

Comments
 (0)