Build and Push Docker Container / build-and-push (push) Successful in 2m25s
- Require Python 3.15 for the server and Python 3.14 for the CLI. - Match the server requirement to quart_common's Python 3.15 features. - Replace future annotations with native deferred evaluation. - Defer CLI command dependencies and QR imports on Python 3.15. - Enable argparse suggestions for mistyped CLI commands. - Remove the obsolete tomllib import fallback. - Update my_helpers and quart_common submodule references. - Lock test dependencies and exclude them from Docker installations. - Add regression coverage for lazy imports and deferred annotations. - Document runtime requirements and standalone CLI validation. - Validate 276 tests on Python 3.15 and 18 CLI tests on Python 3.14. BREAKING CHANGE: The server now requires Python 3.15+ and the standalone CLI requires Python 3.14+.
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
import base64
|
|
import mimetypes
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
import httpx
|
|
|
|
def content_type(path: Path) -> str:
|
|
return mimetypes.guess_type(path.name)[0] or 'application/octet-stream'
|
|
|
|
def upload(client, node: str, path: Path, remote_name: str, note: str, expires: str, remote_path: str = '') -> dict:
|
|
content_b64 = base64.b64encode(path.read_bytes()).decode('ascii')
|
|
params = {
|
|
'file_name': remote_name,
|
|
'content_b64': content_b64,
|
|
'content_type': content_type(path),
|
|
'note': note,
|
|
}
|
|
if remote_path:
|
|
params['file_path'] = remote_path
|
|
if expires:
|
|
params['expires'] = expires
|
|
result = client.call(node, 'files.upload', params)
|
|
return result if isinstance(result, dict) else {'result': result}
|
|
|
|
def update_remote(client, node: str, file_id: str, file_name: str, file_path: str, note: str | None, expires: str) -> None:
|
|
params = {
|
|
'file_id': file_id,
|
|
'file_name': file_name,
|
|
'file_path': file_path,
|
|
}
|
|
if note is not None:
|
|
params['note'] = note
|
|
if expires:
|
|
params['expires'] = expires
|
|
client.call(node, 'files.update', params)
|
|
|
|
def delete_remote(client, node: str, file_id: str) -> None:
|
|
client.call(node, 'files.delete', {'file_id': file_id})
|
|
|
|
def list_remote(client, node: str) -> list[dict]:
|
|
result = client.call(node, 'files.list', {})
|
|
files = result.get('files', []) if isinstance(result, dict) else []
|
|
return files if isinstance(files, list) else []
|
|
|
|
def node_url(client, node: str, path: str) -> str:
|
|
base = client.registry.get(node)
|
|
if not base:
|
|
raise RuntimeError(f'unknown node: {node}')
|
|
return f"{base.rstrip('/')}{path}"
|
|
|
|
def download_url(client, node: str, file_id: str) -> str:
|
|
return node_url(client, node, f"/api/files/{quote(file_id, safe='')}/download")
|
|
|
|
def events_url(client, node: str) -> str:
|
|
return node_url(client, node, '/api/files/events')
|
|
|
|
def download(client, node: str, file_id: str, path: Path) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
headers = {'Accept': '*/*', **client.auth_headers()}
|
|
with httpx.Client(timeout=client.timeout) as http:
|
|
with http.stream('GET', download_url(client, node, file_id), headers=headers) as response:
|
|
if response.status_code == 404:
|
|
raise FileNotFoundError(file_id)
|
|
if response.status_code == 410:
|
|
raise RuntimeError(f'remote file expired: {file_id}')
|
|
response.raise_for_status()
|
|
tmp = path.with_name(f'{path.name}.{Path.cwd().stat().st_ino}.tmp')
|
|
with tmp.open('wb') as handle:
|
|
for chunk in response.iter_bytes():
|
|
handle.write(chunk)
|
|
tmp.replace(path)
|