Compare commits
10 Commits
e11a45ab70
...
b57ca2c47a
| Author | SHA1 | Date | |
|---|---|---|---|
| b57ca2c47a | |||
| 5774ef7779 | |||
| 82c32c2a0b | |||
| 4cc68e2535 | |||
| b87bcdbb53 | |||
| a3e81068c1 | |||
| 5387ce070c | |||
| 7c0d33282a | |||
| f78f495dbc | |||
| bab4e68d4b |
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Python Debugger: Current File with Arguments",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"args": "${command:pickArgs}"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
AGENTS.md
12
AGENTS.md
@ -27,7 +27,7 @@ python main.py /path/to/archive.zip
|
||||
No test framework is currently configured. To add tests, install pytest:
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
uv add pytest
|
||||
```
|
||||
|
||||
Run all tests:
|
||||
@ -47,7 +47,7 @@ No linting tool is currently configured. Recommended tools:
|
||||
|
||||
```bash
|
||||
# Install ruff (fast linter/formatter)
|
||||
pip install ruff
|
||||
uv add ruff
|
||||
|
||||
# Run linting
|
||||
ruff check .
|
||||
@ -59,17 +59,11 @@ ruff check --fix .
|
||||
ruff format .
|
||||
```
|
||||
|
||||
Alternatively, using pylint:
|
||||
```bash
|
||||
pip install pylint
|
||||
pylint main.py
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
# Install mypy
|
||||
pip install mypy
|
||||
uv add mypy
|
||||
|
||||
# Run type checking
|
||||
mypy main.py
|
||||
|
||||
104
frontend.html
Normal file
104
frontend.html
Normal file
@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>File Server</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; overflow: hidden; background: #1a1a1a; }
|
||||
#container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
img { max-width: 100%; max-height: 100%; cursor: pointer; }
|
||||
.chevron {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 48px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
user-select: none;
|
||||
transition: color 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chevron:hover { color: rgba(255, 255, 255, 0.9); }
|
||||
.chevron.left { left: 10px; }
|
||||
.chevron.right { right: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<a href="#" id="img-link"><img id="img" alt="Random image"></a>
|
||||
<a href="#" class="chevron left" id="prev-btn">‹</a>
|
||||
<a href="#" class="chevron right" id="next-btn">›</a>
|
||||
</div>
|
||||
<script>
|
||||
let currentData = null;
|
||||
|
||||
function loadImageSrc(hash) {
|
||||
document.getElementById('img').src = '/' + hash + '/data';
|
||||
document.getElementById('img-link').href = '#';
|
||||
history.replaceState(null, '', '#' + hash);
|
||||
}
|
||||
|
||||
async function loadInfo(hash) {
|
||||
const response = await fetch('/' + hash);
|
||||
if (!response.ok) {
|
||||
loadRandom();
|
||||
return;
|
||||
}
|
||||
currentData = await response.json();
|
||||
loadImageSrc(currentData.img);
|
||||
document.getElementById('img').title = currentData.filename || '';
|
||||
document.getElementById('prev-btn').href = '#' + currentData.previous;
|
||||
document.getElementById('next-btn').href = '#' + currentData.next;
|
||||
}
|
||||
|
||||
async function loadRandom() {
|
||||
const response = await fetch('/random?' + Date.now());
|
||||
const data = await response.json();
|
||||
await loadInfo(data.img);
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', function() {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
loadInfo(hash);
|
||||
} else {
|
||||
loadRandom();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', function() {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) loadInfo(hash);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
e.preventDefault();
|
||||
if (e.code === 'Space') {
|
||||
window.location.hash = '';
|
||||
} else if (e.code === 'ArrowLeft') {
|
||||
document.getElementById('prev-btn').click();
|
||||
} else if (e.code === 'ArrowRight') {
|
||||
document.getElementById('next-btn').click();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
loadInfo(hash);
|
||||
} else {
|
||||
loadRandom();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
199
main.py
199
main.py
@ -1,31 +1,32 @@
|
||||
from io import BytesIO
|
||||
import argparse
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import hashlib
|
||||
import argparse
|
||||
import secrets
|
||||
import zipfile
|
||||
from glob import glob
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
import mimetypes
|
||||
|
||||
app = FastAPI()
|
||||
indexer = None
|
||||
file_mapping = {}
|
||||
indexers = []
|
||||
|
||||
|
||||
class FileIndexer:
|
||||
def __init__(self, path: str):
|
||||
def __init__(self, path: str, salt: str | None = None):
|
||||
self.path = Path(path)
|
||||
self.file_mapping = {}
|
||||
self._salt = None
|
||||
self._index()
|
||||
self._salt = salt
|
||||
self._file_mapping = self._index()
|
||||
|
||||
@property
|
||||
def salt(self) -> str:
|
||||
"""Generate a random salt for hashing"""
|
||||
if not self._salt:
|
||||
if self._salt is None:
|
||||
self._salt = secrets.token_hex(16)
|
||||
return self._salt
|
||||
|
||||
@ -33,132 +34,184 @@ class FileIndexer:
|
||||
"""Generate a salted hash of the file path"""
|
||||
return hashlib.sha256((filepath + self.salt).encode()).hexdigest()
|
||||
|
||||
def _index(self):
|
||||
"""Index all files in the zip file"""
|
||||
def _index(self) -> dict[str, str]:
|
||||
"""Index all files in the directory"""
|
||||
mapping = {}
|
||||
for root, _, files in os.walk(self.path):
|
||||
for file in files:
|
||||
filepath = os.path.join(root, file)
|
||||
# Generate hash for the file path
|
||||
file_hash = self._hash_path(filepath)
|
||||
# Store mapping
|
||||
self.file_mapping[file_hash] = filepath
|
||||
mapping[file_hash] = filepath
|
||||
return mapping
|
||||
|
||||
def get_file_by_hash(self, file_hash: str):
|
||||
"""Get file content by hash"""
|
||||
if file_hash not in self.file_mapping:
|
||||
if file_hash not in self._file_mapping:
|
||||
return None
|
||||
|
||||
file_path = self.file_mapping[file_hash]
|
||||
with open(file_path, 'rb') as f:
|
||||
file_path = self._file_mapping[file_hash]
|
||||
with open(file_path, "rb") as f:
|
||||
yield from f
|
||||
|
||||
def get_filename_by_hash(self, file_hash: str) -> str:
|
||||
def get_filename_by_hash(self, file_hash: str) -> str | None:
|
||||
"""Get filename by hash"""
|
||||
if file_hash not in self.file_mapping:
|
||||
if file_hash not in self._file_mapping:
|
||||
return None
|
||||
return self.file_mapping[file_hash]
|
||||
return self._file_mapping[file_hash]
|
||||
|
||||
|
||||
class ZipFileIndexer(FileIndexer):
|
||||
def _index(self):
|
||||
def _index(self) -> dict[str, str]:
|
||||
"""Index all files in the zip file"""
|
||||
with zipfile.ZipFile(self.path, 'r') as zip_file:
|
||||
self.file_mapping = {
|
||||
self._hash_path(file_info.filename): file_info
|
||||
for file_info in zip_file.infolist()
|
||||
if not file_info.is_dir()
|
||||
}
|
||||
mapping = {}
|
||||
with zipfile.ZipFile(self.path, "r") as zip_file:
|
||||
for file_info in zip_file.infolist():
|
||||
if not file_info.is_dir():
|
||||
file_hash = self._hash_path(file_info.filename)
|
||||
mapping[file_hash] = file_info.filename
|
||||
return mapping
|
||||
|
||||
def get_file_by_hash(self, file_hash: str):
|
||||
"""Get file content by hash"""
|
||||
if file_hash not in self.file_mapping:
|
||||
if file_hash not in self._file_mapping:
|
||||
return None
|
||||
|
||||
file_info = self.file_mapping[file_hash]
|
||||
filename = self._file_mapping[file_hash]
|
||||
|
||||
with zipfile.ZipFile(self.path, 'r') as zip_file:
|
||||
yield from BytesIO(zip_file.read(file_info.filename))
|
||||
with zipfile.ZipFile(self.path, "r") as zip_file:
|
||||
yield from BytesIO(zip_file.read(filename))
|
||||
|
||||
def get_filename_by_hash(self, file_hash: str) -> str:
|
||||
def get_filename_by_hash(self, file_hash: str) -> str | None:
|
||||
"""Get filename by hash"""
|
||||
if file_hash not in self.file_mapping:
|
||||
if file_hash not in self._file_mapping:
|
||||
return None
|
||||
return self.file_mapping[file_hash].filename
|
||||
return self._file_mapping[file_hash]
|
||||
|
||||
INDEXER_MAP = {
|
||||
".zip": ZipFileIndexer
|
||||
}
|
||||
|
||||
INDEXER_MAP = {".zip": ZipFileIndexer}
|
||||
|
||||
|
||||
def initialize_server(args: argparse.Namespace):
|
||||
"""Initialize the server with directory indexing"""
|
||||
global indexer
|
||||
"""Initialize the server with directory or glob indexing"""
|
||||
global file_mapping, indexers
|
||||
|
||||
src_path = Path(args.source)
|
||||
if not src_path.exists():
|
||||
raise SystemExit(f"Source path {src_path} does not exist")
|
||||
|
||||
shared_salt = args.salt
|
||||
if shared_salt is None:
|
||||
shared_salt = secrets.token_hex(16)
|
||||
|
||||
if src_path.is_dir():
|
||||
indexer = FileIndexer(src_path)
|
||||
indexer = FileIndexer(str(src_path), shared_salt)
|
||||
indexers.append(indexer)
|
||||
file_mapping.update(indexer._file_mapping)
|
||||
else:
|
||||
indexer = INDEXER_MAP[src_path.suffix](src_path)
|
||||
pattern = args.source
|
||||
matching_files = glob(pattern)
|
||||
if not matching_files:
|
||||
raise SystemExit(f"No files match pattern {pattern}")
|
||||
|
||||
for file_path in matching_files:
|
||||
file_ext = Path(file_path).suffix
|
||||
if file_ext in INDEXER_MAP:
|
||||
indexer = INDEXER_MAP[file_ext](file_path, shared_salt)
|
||||
indexers.append(indexer)
|
||||
file_mapping.update(indexer._file_mapping)
|
||||
|
||||
print(f"Indexed {len(file_mapping)} files from {len(indexers)} source(s)")
|
||||
|
||||
print(f"Indexed {len(indexer.file_mapping)} files")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Serve the Frontend app"""
|
||||
return FileResponse("frontend.html")
|
||||
|
||||
|
||||
@app.get("/random")
|
||||
async def get_random_file():
|
||||
"""Serve a random file from the mapping"""
|
||||
if not indexer.file_mapping:
|
||||
"""Get random file hashes from the mapping"""
|
||||
if not file_mapping:
|
||||
raise HTTPException(status_code=404, detail="No files indexed")
|
||||
|
||||
random_hash = random.choice(list(indexer.file_mapping.keys()))
|
||||
keys = list(file_mapping.keys())
|
||||
random_idx = random.randint(0, len(keys) - 1)
|
||||
current = keys[random_idx]
|
||||
next_hash = keys[(random_idx + 1) % len(keys)]
|
||||
prev_hash = keys[random_idx - 1] if random_idx > 0 else keys[-1]
|
||||
return {"img": current, "next": next_hash, "previous": prev_hash}
|
||||
|
||||
filename = indexer.get_filename_by_hash(random_hash)
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
if not content_type:
|
||||
content_type = "application/octet-stream"
|
||||
buffer = indexer.get_file_by_hash(random_hash)
|
||||
response = StreamingResponse(
|
||||
buffer,
|
||||
media_type=content_type,
|
||||
headers={
|
||||
"Content-Disposition": f"inline; filename={os.path.basename(filename)}",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0"
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
@app.get("/{file_hash}")
|
||||
async def get_file_by_hash(file_hash: str):
|
||||
def _find_indexer_for_hash(file_hash: str):
|
||||
"""Find the indexer that contains the file with the given hash"""
|
||||
for idx in indexers:
|
||||
if file_hash in idx._file_mapping:
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/{file_hash}/data")
|
||||
async def get_file_data(file_hash: str):
|
||||
"""Serve a specific file by its hash"""
|
||||
if file_hash not in indexer.file_mapping:
|
||||
if file_hash not in file_mapping:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
file_path, content_type, buffer = indexer.get_file_by_hash(file_hash)
|
||||
indexer = _find_indexer_for_hash(file_hash)
|
||||
if not indexer:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
filename = indexer.get_filename_by_hash(file_hash)
|
||||
content_type, _ = mimetypes.guess_type(filename or "")
|
||||
if not content_type:
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
return StreamingResponse(
|
||||
buffer,
|
||||
indexer.get_file_by_hash(file_hash),
|
||||
media_type=content_type,
|
||||
headers={
|
||||
"Content-Disposition": f"inline; filename={os.path.basename(file_path)}",
|
||||
}
|
||||
"Content-Disposition": f"inline; filename={os.path.basename(filename or '')}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/{file_hash}")
|
||||
async def get_file_info(file_hash: str):
|
||||
"""Get file info by hash"""
|
||||
if file_hash not in file_mapping:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
keys = list(file_mapping.keys())
|
||||
idx = keys.index(file_hash)
|
||||
next_hash = keys[(idx + 1) % len(keys)]
|
||||
prev_hash = keys[idx - 1] if idx > 0 else keys[-1]
|
||||
|
||||
indexer = _find_indexer_for_hash(file_hash)
|
||||
if not indexer:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
filename = indexer.get_filename_by_hash(file_hash)
|
||||
return {"img": file_hash, "next": next_hash, "previous": prev_hash, "filename": filename}
|
||||
|
||||
|
||||
# Optional: Add a health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "file_count": len(file_mapping)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Run the file server")
|
||||
parser.add_argument("source", type=str, help="Path to directory or ZIP archive")
|
||||
parser.add_argument(
|
||||
"source",
|
||||
type=str,
|
||||
help="Path to directory, ZIP archive, or glob pattern (e.g., *.zip, path/to/zips/*.zip)",
|
||||
)
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
|
||||
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
||||
parser.add_argument("--salt", type=str, default=None, help="Salt for hashing file paths")
|
||||
args = parser.parse_args()
|
||||
|
||||
initialize_server(args)
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@ -8,3 +8,24 @@ dependencies = [
|
||||
"fastapi>=0.128.0",
|
||||
"uvicorn>=0.40.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff>=0.15.5",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM"]
|
||||
ignore = []
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.15.5",
|
||||
]
|
||||
|
||||
42
uv.lock
generated
42
uv.lock
generated
@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
@ -77,11 +77,26 @@ dependencies = [
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.128.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.5" },
|
||||
{ name = "uvicorn", specifier = ">=0.40.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.15.5" }]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
@ -169,6 +184,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.50.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user