Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 5 additions & 26 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v4.4.0"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.13
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml

- repo: https://github.com/asottile/add-trailing-comma
rev: "v2.4.0"
hooks:
- id: add-trailing-comma
args:
- "--py36-plus"

- repo: https://github.com/PyCQA/isort
rev: "5.12.0"
hooks:
- id: isort

- repo: https://github.com/pycqa/flake8
rev: "6.0.0"
hooks:
- id: flake8
args:
- "--max-line-length=100"
- '--ignore=E501,E226,W503,E402'
- id: ruff-check
args: ["--fix", "--select=I"]
- id: ruff-format
10 changes: 2 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,8 @@ simple-repository-browser = "simple_repository_browser.__main__:main"
[tool.setuptools_scm]
version_file = "simple_repository_browser/_version.py"

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 88
force_sort_within_sections = true
[tool.ruff.lint.isort]
force-sort-within-sections = true

# [tool.mypy]
# check_untyped_defs = true
Expand Down
1 change: 1 addition & 0 deletions simple_repository_browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Documentation for the simple_repository_browser package

"""

from . import _version # type: ignore

__version__ = _version.version # type: ignore
40 changes: 27 additions & 13 deletions simple_repository_browser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,25 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
# parsed arguments.
parser.set_defaults(handler=handler)

parser.add_argument("repository_url", type=str, nargs='?', default='https://pypi.org/simple/')
parser.add_argument(
"repository_url", type=str, nargs="?", default="https://pypi.org/simple/"
)
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--cache-dir", type=str, default=Path(os.environ.get('XDG_CACHE_DIR', Path.home() / '.cache')) / 'simple-repository-browser')
parser.add_argument(
"--cache-dir",
type=str,
default=Path(os.environ.get("XDG_CACHE_DIR", Path.home() / ".cache"))
/ "simple-repository-browser",
)
parser.add_argument("--url-prefix", type=str, default="")
parser.add_argument('--no-popular-project-crawl', dest='crawl_popular_projects', action='store_false', default=True)
parser.add_argument('--templates-dir', default=here / "templates", type=Path)
parser.add_argument(
"--no-popular-project-crawl",
dest="crawl_popular_projects",
action="store_false",
default=True,
)
parser.add_argument("--templates-dir", default=here / "templates", type=Path)


def handler(args: typing.Any) -> None:
Expand All @@ -38,9 +50,9 @@ def handler(args: typing.Any) -> None:
# Include the base templates so that the given templates directory doesn't have to
# implement *all* of the templates. This must be at a lower precedence than the given
# templates path, so that they can be overriden.
here/"templates"/"base",
here / "templates" / "base",
# Include the "base" folder, such that upstream templates can inherit from "base/...".
here/"templates",
here / "templates",
],
static_files_paths=[here / "static"],
crawl_popular_projects=args.crawl_popular_projects,
Expand All @@ -49,10 +61,12 @@ def handler(args: typing.Any) -> None:
).create_app()

log_conf = LOGGING_CONFIG.copy()
log_conf["formatters"]["default"]["fmt"] = "%(asctime)s [%(name)s] %(levelprefix)s %(message)s"
log_conf["formatters"]["access"][
"fmt"
] = '%(asctime)s [%(name)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
log_conf["formatters"]["default"]["fmt"] = (
"%(asctime)s [%(name)s] %(levelprefix)s %(message)s"
)
log_conf["formatters"]["access"]["fmt"] = (
'%(asctime)s [%(name)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
)

uvicorn.run(
app=app,
Expand All @@ -71,8 +85,8 @@ def main():
args.handler(args)


if __name__ == '__main__':
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.getLogger('httpcore').setLevel(logging.WARNING)
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
main()
35 changes: 24 additions & 11 deletions simple_repository_browser/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def __init__(
self.crawl_popular_projects = crawl_popular_projects
self.browser_version = browser_version

self.cache = diskcache.Cache(str(cache_dir/'diskcache'))
self.db_path = cache_dir / 'projects.sqlite'
self.cache = diskcache.Cache(str(cache_dir / "diskcache"))
self.db_path = cache_dir / "projects.sqlite"
self.con = sqlite3.connect(
self.db_path,
detect_types=sqlite3.PARSE_DECLTYPES,
Expand All @@ -51,7 +51,6 @@ def create_app(self) -> fastapi.FastAPI:
_view = self.create_view()

async def lifespan(app: fastapi.FastAPI):

async with (
httpx.AsyncClient(timeout=30) as http_client,
aiosqlite.connect(self.db_path, timeout=5) as db,
Expand All @@ -72,7 +71,9 @@ async def lifespan(app: fastapi.FastAPI):
# convenient for development purposes.
@app.get("/")
async def redirect_to_index():
return fastapi.responses.RedirectResponse(url=app.url_path_for('index'))
return fastapi.responses.RedirectResponse(
url=app.url_path_for("index")
)

yield

Expand All @@ -92,7 +93,7 @@ async def catch_exceptions_middleware(request: fastapi.Request, call_next):
detail = f"Internal server error ({err})"
# raise
logging.getLogger("simple_repository_browser.error").error(
'Unhandled exception',
"Unhandled exception",
exc_info=err,
)
content = _view.error_page(
Expand All @@ -104,14 +105,20 @@ async def catch_exceptions_middleware(request: fastapi.Request, call_next):
status_code=status_code,
)

app.middleware('http')(catch_exceptions_middleware)
app.middleware("http")(catch_exceptions_middleware)

return app

def create_view(self) -> view.View:
return view.View(self.template_paths, self.browser_version, static_files_manifest=self.static_files_manifest)
return view.View(
self.template_paths,
self.browser_version,
static_files_manifest=self.static_files_manifest,
)

def create_crawler(self, http_client: httpx.AsyncClient, source: SimpleRepository) -> crawler.Crawler:
def create_crawler(
self, http_client: httpx.AsyncClient, source: SimpleRepository
) -> crawler.Crawler:
return crawler.Crawler(
http_client=http_client,
crawl_popular_projects=self.crawl_popular_projects,
Expand All @@ -120,7 +127,9 @@ def create_crawler(self, http_client: httpx.AsyncClient, source: SimpleRepositor
cache=self.cache,
)

def _repo_from_url(self, url: str, http_client: httpx.AsyncClient) -> SimpleRepository:
def _repo_from_url(
self, url: str, http_client: httpx.AsyncClient
) -> SimpleRepository:
if urlparse(url).scheme in ("http", "https"):
return HttpRepository(
url=url,
Expand All @@ -129,7 +138,9 @@ def _repo_from_url(self, url: str, http_client: httpx.AsyncClient) -> SimpleRepo
else:
return LocalRepository(Path(url))

def create_model(self, http_client: httpx.AsyncClient, database: aiosqlite.Connection) -> model.Model:
def create_model(
self, http_client: httpx.AsyncClient, database: aiosqlite.Connection
) -> model.Model:
source = MetadataInjector(
self._repo_from_url(self.repository_url, http_client=http_client),
http_client=http_client,
Expand All @@ -141,7 +152,9 @@ def create_model(self, http_client: httpx.AsyncClient, database: aiosqlite.Conne
crawler=self.create_crawler(http_client, source),
)

def create_controller(self, view: view.View, model: model.Model) -> controller.Controller:
def create_controller(
self, view: view.View, model: model.Model
) -> controller.Controller:
return controller.Controller(
model=model,
view=view,
Expand Down
30 changes: 15 additions & 15 deletions simple_repository_browser/_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ class Term:


class FilterOn(Enum):
name = 'name'
summary = 'summary'
name_or_summary = 'name_or_summary'
name = "name"
summary = "summary"
name_or_summary = "name_or_summary"


@dataclasses.dataclass
Expand Down Expand Up @@ -57,8 +57,8 @@ def prepare_name(term: Filter) -> SafeSQLStmt:
value = term.value[1:-1]
else:
value = normalise_name(term.value)
value = value.replace('*', '%')
return "canonical_name LIKE ?", (f'%{value}%',)
value = value.replace("*", "%")
return "canonical_name LIKE ?", (f"%{value}%",)


def prepare_summary(term: Filter) -> SafeSQLStmt:
Expand All @@ -67,16 +67,16 @@ def prepare_summary(term: Filter) -> SafeSQLStmt:
value = term.value[1:-1]
else:
value = term.value
value = value.replace('*', '%')
return "summary LIKE ?", (f'%{value}%',)
value = value.replace("*", "%")
return "summary LIKE ?", (f"%{value}%",)


def build_sql(term: typing.Union[Term, typing.Tuple[Term, ...]]) -> SafeSQLStmt:
# Return query and params to be used in SQL. query MUST not be produced using untrusted input, as is vulnerable to SQL injection.
# Instead, any user input must be in the parameters, which undergoes sqllite built-in cleaning.
if isinstance(term, tuple):
if len(term) == 0:
return '', ()
return "", ()

# No known query can produce a multi-value term
assert len(term) == 1
Expand All @@ -103,7 +103,7 @@ def build_sql(term: typing.Union[Term, typing.Tuple[Term, ...]]) -> SafeSQLStmt:
return f"({sql1} OR {sql2})", terms1 + terms2
elif isinstance(term, Not):
sql1, terms1 = build_sql(term.term)
return f'(Not {sql1})', terms1
return f"(Not {sql1})", terms1
else:
raise ValueError(f"unknown term type {type(term)}")

Expand Down Expand Up @@ -145,11 +145,11 @@ def query_to_sql(query) -> SafeSQLStmt:
| -> ())
"""),
{
'And': And,
'Or': Or,
'Filter': Filter,
'Not': Not,
'FilterOn': FilterOn,
"And": And,
"Or": Or,
"Filter": Filter,
"Not": Not,
"FilterOn": FilterOn,
},
)

Expand All @@ -166,7 +166,7 @@ def simple_name_from_query(terms: typing.Tuple[Term, ...]) -> typing.Optional[st
for term in terms:
if isinstance(term, Filter):
if term.filter_on in [FilterOn.name_or_summary, FilterOn.name]:
if '*' in term.value or '"' in term.value:
if "*" in term.value or '"' in term.value:
break
return normalise_name(term.value)
else:
Expand Down
Loading