Initial scaffold: Flask/SQLite backend, React/Vite PWA frontend
Backend: receipt upload -> LLM vision extraction (OpenAI-compatible, provider-agnostic), item review/edit, per-group splitting against Cospend projects/members, highlight+upload via WebDAV, public share link, bill creation via Cospend's OCS API (verified against real source, not just doc summaries). Frontend: capture -> review -> group -> summary flow as an installable PWA. install.sh / run.sh (venv + npm, tmux session) instead of Docker, per the ~/Projects/gain pattern.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""Thin wrapper around the bits of Nextcloud's WebDAV and OCS Share API we need.
|
||||
|
||||
Auth is HTTP Basic with an app password (Settings -> Security -> Devices &
|
||||
Sessions -> create app password), never the account's real password.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
def _auth() -> tuple[str, str]:
|
||||
return (Config.NC_USERNAME, Config.NC_APP_PASSWORD)
|
||||
|
||||
|
||||
def _webdav_root() -> str:
|
||||
return f"{Config.NC_BASE_URL}/remote.php/dav/files/{Config.NC_USERNAME}"
|
||||
|
||||
|
||||
def ensure_upload_folder() -> None:
|
||||
"""MKCOL each segment of the configured upload folder path if missing.
|
||||
|
||||
NC_UPLOAD_FOLDER may be nested (e.g. "Documents/Cospend/Assets"); MKCOL
|
||||
only creates one level at a time, so walk the path segment by segment.
|
||||
Idempotent.
|
||||
"""
|
||||
if not Config.NC_UPLOAD_FOLDER:
|
||||
return
|
||||
segments = Config.NC_UPLOAD_FOLDER.split("/")
|
||||
partial = ""
|
||||
for segment in segments:
|
||||
partial = f"{partial}/{segment}" if partial else segment
|
||||
url = f"{_webdav_root()}/{partial}"
|
||||
resp = requests.request("MKCOL", url, auth=_auth(), timeout=15)
|
||||
# 201 = created, 405 = already exists. Anything else is a real problem.
|
||||
if resp.status_code not in (201, 405):
|
||||
resp.raise_for_status()
|
||||
|
||||
|
||||
def upload_file(filename: str, content: bytes, content_type: str = "image/jpeg") -> str:
|
||||
"""Uploads `content` to <upload folder>/<filename> via WebDAV PUT.
|
||||
|
||||
Returns the server-relative path (e.g. "wgBill/receipt-123.jpg"), which is
|
||||
what the Share API expects as `path`.
|
||||
"""
|
||||
ensure_upload_folder()
|
||||
rel_path = f"{Config.NC_UPLOAD_FOLDER}/{filename}" if Config.NC_UPLOAD_FOLDER else filename
|
||||
url = f"{_webdav_root()}/{rel_path}"
|
||||
resp = requests.put(
|
||||
url, data=content, auth=_auth(), headers={"Content-Type": content_type}, timeout=30
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return rel_path
|
||||
|
||||
|
||||
def create_public_share(rel_path: str) -> str:
|
||||
"""Creates a public read-only link share for `rel_path`. Returns the share URL."""
|
||||
url = f"{Config.NC_BASE_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares"
|
||||
resp = requests.post(
|
||||
url,
|
||||
auth=_auth(),
|
||||
headers={"OCS-APIRequest": "true"},
|
||||
data={
|
||||
"path": f"/{rel_path}",
|
||||
"shareType": 3, # public link
|
||||
"permissions": 1, # read-only
|
||||
},
|
||||
params={"format": "json"},
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
share_url = payload["ocs"]["data"]["url"]
|
||||
return share_url
|
||||
Reference in New Issue
Block a user